Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7df4a578b | |||
| 38e30b2525 | |||
| 8e44b71614 | |||
| 11bca28ef5 | |||
| 4ef0c6021a | |||
| 4fbbd58ae9 | |||
| 81dea6cf90 | |||
| 9b57fb280b | |||
| e804d0b31e | |||
| 76845fc7e4 | |||
| decd7f7918 | |||
| 8c5ff4f230 | |||
| 535600edc8 | |||
| 0848702f65 | |||
| 5379374135 | |||
| b7772128d7 | |||
| 95a1c7f92c | |||
| 2d69cd580a | |||
| 34552926d1 | |||
| 739f584921 | |||
| 04d8b6421a | |||
| fdcd1f3d28 | |||
| fc02dc02e8 | |||
| 6955d83b0c | |||
| 1b3d3a867b | |||
| 83718d7b31 | |||
| e80627a3c3 | |||
| e1003d4bb6 | |||
| 2ef4a52be0 | |||
| 93a63d3714 | |||
| a8d9d98cf2 | |||
| d70ddd1f53 | |||
| 283c7c5a26 | |||
| fb5f50d5bd | |||
| 06e84b41aa | |||
| e3f262322a | |||
| 70c2fa8ef4 | |||
| 9f534e18c8 | |||
| 55bd72aef8 | |||
| c1a23faf1f | |||
| 3588c297f8 | |||
| 04d03cbc8f | |||
| 4e27efb6a1 | |||
| 59b3e5bb24 | |||
| d8eee3d81a | |||
| c8926682b2 | |||
| 9117a9d779 | |||
| 4ea1775f2c | |||
| a8020ecebe | |||
| 2ace076fce | |||
| 45e897d475 | |||
| 98676f0573 | |||
| c966ab9a52 | |||
| ebaf11ad10 | |||
| 19c7ba03c6 | |||
| 894b5c5c6c | |||
| 516e93cee2 | |||
| cc0ffc6e60 | |||
| a97ace6e73 | |||
| 6d49463dad | |||
| 81e6e4e5f2 | |||
| 2adb355183 | |||
| 5e6c53432b | |||
| 873f77bc43 | |||
| 9bf098a93a | |||
| 388713a3c6 | |||
| e94dd58542 | |||
| d985a1c588 | |||
| dbac6e8918 | |||
| a481c0ee5e | |||
| eef6fdaeb3 | |||
| b8b1a5bba6 | |||
| f06f52fce7 | |||
| 4a332bb77b | |||
| eb1b202566 | |||
| 658f3a1a09 | |||
| 55eba480ac | |||
| bbeea5b0ec | |||
| ad454a94ef | |||
| 268215ff5f | |||
| 4e70daa4d8 | |||
| bb28f49cf5 | |||
| d85211a145 | |||
| a7291d374d | |||
| 5c9b558ac2 | |||
| 36ede22d45 | |||
| 6528ec4056 | |||
| 56ee494c7d | |||
| b21995a0b9 | |||
| 3c00575ecd | |||
| 27ccbcb54a | |||
| fecbf394c1 | |||
| 91341e2d21 | |||
| 6349503b00 | |||
| 58e8c103b7 | |||
| 5d115afa71 | |||
| d8b308a18c | |||
| 76267c00d5 | |||
| 9648856052 | |||
| d87e465a8e | |||
| 2c07d6719e | |||
| 4c633eb60d | |||
| ba6580e4ef | |||
| c21d8f837e | |||
| eadfa09570 | |||
| ea1a0b7fc8 | |||
| 9f797613d2 | |||
| b728ff33ec | |||
| 7dc036c6e4 | |||
| 78135aac02 | |||
| 950018673f | |||
| cfdcf05135 | |||
| ace474eb2c | |||
| 285ed8d56e | |||
| 738e25feda | |||
| 6d2d071293 | |||
| 725ce50608 | |||
| 78e884e97e | |||
| cb123cb575 | |||
| 6f3081cb8e | |||
| 231f734fd5 | |||
| fce7325a24 | |||
| 2bec45411f | |||
| 577195b578 | |||
| a402227c4f | |||
| a75b790654 | |||
| f07cbeac52 | |||
| dcfcce7803 | |||
| 659868181d | |||
| d76e6444e0 | |||
| 0dbbf4840c | |||
| 1b6af9fc08 | |||
| 8e1541ea56 | |||
| fd9908833a | |||
| 24f8300b2c | |||
| 8d510f5d90 | |||
| 6457680065 | |||
| 3175911105 | |||
| 00f26bdc75 | |||
| 9db95bb772 | |||
| e1ba96784c | |||
| f67d1d41cb | |||
| bb7367615d | |||
| f8be8fb583 | |||
| e00393936f | |||
| 3c782de64d | |||
| 678dc9ef6b | |||
| 67bb9cd4a5 | |||
| 51cfb9062a | |||
| 1ecf979721 | |||
| 642b0fdc95 | |||
| bc64d6886a | |||
| 81399c59f7 | |||
| 69d10ef429 | |||
| 3c616f4f6f | |||
| 988b61e459 | |||
| 3d4e0b8fc0 | |||
| 564fcfca61 | |||
| 709e1da768 | |||
| a1f281d8b4 | |||
| d2f3999cf1 | |||
| 87fc9f2fb9 | |||
| 8c9064fd93 | |||
| 561849ae5b | |||
| 0847802ce4 | |||
| d5a8b3f1fb | |||
| e6cebd8c46 | |||
| f2be036bac | |||
| f14448d40d | |||
| bf719808f2 | |||
| 9dd82c91d7 | |||
| 535f84064a | |||
| 0c0a55d766 | |||
| 6e3ee29eb4 | |||
| 6a7a5dc7a3 | |||
| e78d2d79d0 | |||
| 451027eaf3 | |||
| e4491610fb | |||
| f30e10f235 | |||
| f9249b1380 | |||
| 3df94526b0 | |||
| b30b7b1bd3 | |||
| a9defd67d6 | |||
| 68d346e69d | |||
| e2fd27cbba | |||
| 4c0532006c | |||
| 7ac574b230 | |||
| 7eb855de8f | |||
| d5984f4141 | |||
| b7c0c85639 | |||
| 84ba166aea | |||
| bd79858681 | |||
| 0f10fa3991 | |||
| 74b1799d21 | |||
| 4552643ff8 | |||
| d432b388f6 | |||
| a8475602c7 | |||
| f58d33af9e | |||
| 0150ea5e70 | |||
| 3bf43f1606 | |||
| b8729a6ec7 | |||
| 1f44aa7e85 | |||
| 2bd5352fc5 | |||
| a90130e8bf | |||
| 642e8796f0 | |||
| 615cbddc89 | |||
| 4ef82bdff4 | |||
| dafde04c2c | |||
| 1be61b8d89 | |||
| c3215c7425 | |||
| af0cd26ea0 | |||
| cb7dacd089 | |||
| 8c04971094 | |||
| 3a4802f09a | |||
| d78db306c5 | |||
| 3f8790ece1 | |||
| f9e6158144 | |||
| 05de3fed15 | |||
| 38cba9cb39 | |||
| a4af980e11 | |||
| 940b844857 | |||
| 41b766216e | |||
| 402987baba | |||
| 3cb08c73d3 | |||
| 4cb92a7257 | |||
| a095768eae | |||
| 1a5925d7e8 | |||
| 9147847710 | |||
| 05fe8bcaca | |||
| b0c3c6f45a | |||
| 0f641aa852 | |||
| 2651bbe50c | |||
| d31371eb6c | |||
| ec0e7e5ec7 | |||
| feb75a8a42 | |||
| d4369d2503 | |||
| d236589644 | |||
| 8044b7f623 | |||
| 9f0697dd34 | |||
| 78a6f3122d | |||
| b460da74dd | |||
| 75a8bb7962 | |||
| 9ac876e30a | |||
| 26cb4ea034 | |||
| 0d65ee1a32 | |||
| 4a753376b7 | |||
| dc926e9f5a | |||
| 722372c7f6 | |||
| 4589c6ee0a | |||
| 67ff93e640 | |||
| bd055d704b | |||
| 2e8bee931c | |||
| a454a4f4a8 | |||
| 45541a3cdd | |||
| 1d42d922bd | |||
| 4f631fbd0e | |||
| e911db4c1a | |||
| 9b60147e11 | |||
| acd0cabdff | |||
| d41f6058f7 | |||
| 8f835eec4e | |||
| ecab525ffd | |||
| 7c887e8ec1 | |||
| f3a23a528b | |||
| cdcb31130b | |||
| 3ea24ddf0c | |||
| 12baadd563 | |||
| f5ae36d4e7 | |||
| 04ca738fb1 | |||
| 95e09e51e1 | |||
| 2f0af385c7 | |||
| 786e6d5799 | |||
| 61c5df750a | |||
| eb30afcb83 | |||
| cdf0f6e96c | |||
| 54158c5dbe | |||
| 56ff86db44 | |||
| b7560c80aa | |||
| 03379943de | |||
| 2376fd8968 | |||
| 2f90193d7e | |||
| 964199f8a9 | |||
| 678ea20004 | |||
| ea27fd8a45 | |||
| 38eef3f0ad | |||
| 22615e9ce9 | |||
| a999abfbf8 | |||
| 20c1d3ef08 | |||
| b06c8e4918 | |||
| 6edfdcefcc | |||
| 10b145b006 | |||
| 0ba9a9659d | |||
| 2dfa1b6b14 | |||
| 7a3f9f1fa4 | |||
| f276fdc6a0 | |||
| 7963bdd1e4 | |||
| 195c57edc3 | |||
| 4442c85dc1 | |||
| 5bcac2a2b0 | |||
| 5303b67d11 | |||
| af59e9abb8 | |||
| fb098c9147 | |||
| 739974bef4 | |||
| d21e48a1a3 | |||
| 8fea0cbe77 | |||
| 1e2b8efb13 | |||
| 8495963094 | |||
| 06d1c0bc3b | |||
| 5965c2e237 | |||
| fb34dfadb0 | |||
| 13b0ac737b | |||
| 300430b3ec | |||
| cf6f154e6e | |||
| 2ddf8c0cdb | |||
| 2a402f77b5 | |||
| 7b2c31658a | |||
| 7a91a60af9 | |||
| bfa6c70bf3 | |||
| 73eff05180 | |||
| 74f3b3f13d | |||
| 181833d768 | |||
| be9523304a | |||
| b26fef3ad4 | |||
| 9f86674bbe | |||
| 095e57a037 | |||
| 66a8e3bb79 | |||
| 473137abdf | |||
| 740f1605e7 | |||
| 0922ec020e | |||
| dbe8291f55 | |||
| 9dcc16277e | |||
| aa611fa6ba | |||
| 083040e300 | |||
| 99e92e4594 | |||
| 870f6e88b1 | |||
| 16d2014bfb | |||
| 4d9a22e82c | |||
| 42d77e445b | |||
| 6506846207 | |||
| 2b9af0e0de | |||
| 762d2927f7 | |||
| d9561f3b12 | |||
| dde24848d4 | |||
| e786482902 | |||
| 4e64922b70 | |||
| 15042b16d1 | |||
| 5e4c4fc6c9 | |||
| 7194c53891 | |||
| 7eff77ccc4 | |||
| 1b78ffaa91 | |||
| 8e8bfd68d1 | |||
| b029505cdd | |||
| c5c862bee3 | |||
| 3c38d008f1 | |||
| dc52b00a00 | |||
| b5d2e7040e | |||
| 5818440721 | |||
| f1c46da47d | |||
| 212c69d303 | |||
| 9e4152e298 | |||
| 307f023e47 | |||
| 3451bd8762 | |||
| a9d0be8aae | |||
| d83f720631 | |||
| 1f3d396296 | |||
| 48f771f344 | |||
| 555bc6aa26 | |||
| 8bd0eaac1e | |||
| 3280c77002 | |||
| b39743a53a | |||
| 9a73da56e9 | |||
| c9b0d2664f | |||
| 6063c9efac | |||
| dd6f192d4a | |||
| d956f4ed3d | |||
| 4728258750 | |||
| ece3e16459 | |||
| 9208dbe2f3 | |||
| 636de18642 | |||
| ee48456291 | |||
| a06d5ffaed | |||
| 606821a2c0 | |||
| 5c980c21e5 | |||
| 771cc380df | |||
| 38217870fe | |||
| 5b82c96a43 | |||
| 6f5f9869ad | |||
| b29bfeb8b1 | |||
| cb40559e49 | |||
| 90c72f7ffe | |||
| 002bd2e6f7 | |||
| 7b44f17a64 | |||
| b5c83f92e3 | |||
| 51b4d64a93 | |||
| 62c9e0a22f | |||
| 3daac34d3e | |||
| d80d5d1632 | |||
| 912f716362 | |||
| 16ecdf41af | |||
| f0bb6b08fa | |||
| efb4e2ce9a | |||
| 03238d10bf | |||
| e71590b9fb | |||
| 4728f1cc46 | |||
| 794778dee2 | |||
| b5e882f07e | |||
| e7c58a4847 | |||
| bdb44db25e | |||
| e8b82ffe62 | |||
| 53c53c009e | |||
| 7e8cda4605 | |||
| dfa0419a0a | |||
| aeb2638d1e | |||
| c5cef56e2a | |||
| b9c9d98252 | |||
| 30083b6705 | |||
| 47db6cf1bd | |||
| f929f6ad7d | |||
| 7e16e0f30c | |||
| b2be4e51cc | |||
| 2c871be8c5 | |||
| 8c03e74979 | |||
| d5c0355fd4 | |||
| 386cad0474 | |||
| 474024ea55 | |||
| dacf13e46d | |||
| f37b4bb2ee | |||
| 034398e9fb | |||
| 2c605cb176 | |||
| 9a6673fe6d | |||
| 6733c9adba | |||
| 9d3443ceac | |||
| d628424b35 | |||
| dab444040e | |||
| ecef854d23 | |||
| 166087e33c | |||
| e9e30c4c46 | |||
| fd400aa850 |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Zipline Codespace",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
"forwardPorts": [3000, 5432],
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {},
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"files.autoSave": "afterDelay"
|
||||
},
|
||||
"extensions": ["prisma.prisma", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
},
|
||||
"remoteUser": "zipline",
|
||||
"remoteEnv": {
|
||||
"CORE_DATABASE_URL": "postgres://postgres:postgres@localhost/zip10"
|
||||
},
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Zipline",
|
||||
"onAutoForward": "openBrowser"
|
||||
},
|
||||
"5432": {
|
||||
"label": "Postgres"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
network_mode: service:db
|
||||
command: sleep infinity
|
||||
user: zipline
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
@@ -0,0 +1,46 @@
|
||||
# every field in here is optional except, CORE_SECRET and CORE_DATABASE_URL.
|
||||
# if CORE_SECRET is still "changethis" then zipline will exit and tell you to change it.
|
||||
|
||||
# if using s3/supabase make sure to comment out the other datasources
|
||||
|
||||
CORE_HTTPS=true
|
||||
CORE_SECRET="changethis"
|
||||
CORE_HOST=0.0.0.0
|
||||
CORE_PORT=3000
|
||||
CORE_DATABASE_URL="postgres://postgres:postgres@localhost/zip10"
|
||||
CORE_LOGGER=false
|
||||
CORE_STATS_INTERVAL=1800
|
||||
|
||||
# default
|
||||
DATASOURCE_TYPE=local
|
||||
DATASOURCE_LOCAL_DIRECTORY=./uploads
|
||||
|
||||
# or you can choose to use s3
|
||||
DATASOURCE_TYPE=s3
|
||||
DATASOURCE_S3_ACCESS_KEY_ID=key
|
||||
DATASOURCE_S3_SECRET_ACCESS_KEY=secret
|
||||
DATASOURCE_S3_BUCKET=bucket
|
||||
DATASOURCE_S3_ENDPOINT=s3.amazonaws.com
|
||||
DATASOURCE_S3_REGION=us-west-2
|
||||
DATASOURCE_S3_FORCE_S3_PATH=false
|
||||
DATASOURCE_S3_USE_SSL=false
|
||||
|
||||
# or supabase
|
||||
DATASOURCE_TYPE=supabase
|
||||
DATASOURCE_SUPABASE_KEY=xxx
|
||||
# remember: no leading slash
|
||||
DATASOURCE_SUPABASE_URL=https://something.supabase.co
|
||||
DATASOURCE_SUPABASE_BUCKET=zipline
|
||||
|
||||
UPLOADER_DEFAULT_FORMAT=RANDOM
|
||||
UPLOADER_ROUTE=/u
|
||||
UPLOADER_LENGTH=6
|
||||
UPLOADER_ADMIN_LIMIT=104900000
|
||||
UPLOADER_USER_LIMIT=104900000
|
||||
UPLOADER_DISABLED_EXTENSIONS=someext
|
||||
|
||||
URLS_ROUTE=/go
|
||||
URLS_LENGTH=6
|
||||
|
||||
RATELIMIT_USER = 5
|
||||
RATELIMIT_ADMIN = 3
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"jsx-quotes": ["error", "prefer-single"],
|
||||
"indent": "off",
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/jsx-uses-react": "warn",
|
||||
"react/jsx-uses-vars": "warn",
|
||||
"react/no-danger-with-children": "warn",
|
||||
"react/no-deprecated": "warn",
|
||||
"react/no-direct-mutation-state": "warn",
|
||||
"react/no-is-mounted": "warn",
|
||||
"react/no-typos": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off",
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Bug
|
||||
description: File a bug report
|
||||
title: 'Bug: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Provide steps to reproduce the bug, and some context.
|
||||
value: 'A bug happened!'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Zipline are you using?
|
||||
options:
|
||||
- upstream (ghcr.io/diced/zipline:trunk)
|
||||
- latest (ghcr.io/diced/zipline:latest)
|
||||
- other (provide version in additional info)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browser(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chromium-based (Chrome, Edge, Brave, Opera, mobile chrome/chromium based, etc)
|
||||
- Safari
|
||||
- Firefox Mobile
|
||||
- Safari Mobile
|
||||
- type: textarea
|
||||
id: zipline-logs
|
||||
attributes:
|
||||
label: Zipline Logs
|
||||
description: Please copy and paste any relevant log output. Not seeing anything interesting? Try adding the `DEBUG=true` environment variable to see more logs, make sure to review the output and remove any sensitive information as it can be VERY verbose at times.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browser-logs
|
||||
attributes:
|
||||
label: Browser Logs
|
||||
description: Please copy and paste any relevant log output.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Anything else that could be used to narrow down the issue, like your config.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: dicedtomatoreal
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. Arch]
|
||||
- Browser [e.g. chrome, firefox, chrome mobile]
|
||||
- Version [e.g. 2.0.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Zipline Discord
|
||||
url: https://discord.gg/EAhCRfGxCF
|
||||
about: Ask for help with anything related to Zipline!
|
||||
- name: Zipline Docs
|
||||
url: https://zipline.diced.tech
|
||||
about: Maybe take a look a the docs?
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 'Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ trunk ]
|
||||
pull_request:
|
||||
branches: [ trunk ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
|
||||
- name: 'Restore dependency cache'
|
||||
id: cache-restore
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}-
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
env:
|
||||
ZIPLINE_DOCKER_BUILD: true
|
||||
@@ -0,0 +1,51 @@
|
||||
name: 'Push Release Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
- 'Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_ghcr:
|
||||
name: Push Release Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:latest
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,50 @@
|
||||
name: 'Push Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ trunk ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
- 'Dockerfile'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_ghcr:
|
||||
name: Push Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:trunk
|
||||
ghcr.io/diced/zipline:trunk-${{ steps.version.outputs.zipline_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,27 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
@@ -1,10 +1,45 @@
|
||||
node_modules
|
||||
temp
|
||||
tmp
|
||||
uploads
|
||||
config*.json
|
||||
out
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
.vscode
|
||||
ssl/localhost.key
|
||||
ssl/localhost.crt
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# zipline
|
||||
config.toml
|
||||
uploads*/
|
||||
dist/
|
||||
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
scripts
|
||||
public
|
||||
views
|
||||
@@ -1 +1,5 @@
|
||||
{}
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 110
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
checksumBehavior: update
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.3.1.cjs
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at . All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1,23 @@
|
||||
# Contributing
|
||||
|
||||
## Bug reports
|
||||
|
||||
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||
|
||||
- The steps to reproduce the bug
|
||||
- Logs of Zipline
|
||||
- The version of Zipline
|
||||
- Your OS & Browser including server OS
|
||||
- What you were expecting to see
|
||||
|
||||
## Feature requests
|
||||
|
||||
Create an discussion on GitHub, please include the following:
|
||||
|
||||
- Breif explanation of the feature in the title (very breif please)
|
||||
- How it would work (detailed, but optional)
|
||||
|
||||
## Pull Requests (contributions to the codebase)
|
||||
|
||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
||||
Please make sure your code also reflects the style of the rest of the code.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Use the Prisma binaries image as the first stage
|
||||
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
|
||||
|
||||
# Use Alpine Linux as the second stage
|
||||
FROM node:18-alpine3.16 as base
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /zipline
|
||||
|
||||
# Copy the necessary files from the project
|
||||
COPY prisma ./prisma
|
||||
COPY src ./src
|
||||
COPY next.config.js ./next.config.js
|
||||
COPY tsup.config.ts ./tsup.config.ts
|
||||
COPY tsconfig.json ./tsconfig.json
|
||||
COPY mimes.json ./mimes.json
|
||||
COPY public ./public
|
||||
|
||||
FROM base as builder
|
||||
|
||||
COPY .yarn ./.yarn
|
||||
COPY package*.json ./
|
||||
COPY yarn*.lock ./
|
||||
COPY .yarnrc.yml ./
|
||||
|
||||
# Copy the prisma binaries from prisma stage
|
||||
COPY --from=prisma /prisma-engines /prisma-engines
|
||||
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
|
||||
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
|
||||
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
|
||||
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
|
||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
||||
PRISMA_CLIENT_ENGINE_TYPE=binary \
|
||||
ZIPLINE_DOCKER_BUILD=true \
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Install production dependencies then temporarily save
|
||||
RUN yarn workspaces focus --production --all
|
||||
RUN cp -RL node_modules /tmp/node_modules
|
||||
|
||||
# Install the dependencies
|
||||
RUN yarn install --immutable
|
||||
|
||||
# Run the build
|
||||
RUN yarn build
|
||||
|
||||
# Use Alpine Linux as the final image
|
||||
FROM base
|
||||
# Install the necessary packages
|
||||
RUN apk add --no-cache perl procps tini
|
||||
|
||||
COPY --from=builder /prisma-engines /prisma-engines
|
||||
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
|
||||
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
|
||||
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
|
||||
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
|
||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
||||
PRISMA_CLIENT_ENGINE_TYPE=binary \
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Copy only the necessary files from the previous stage
|
||||
COPY --from=builder /zipline/dist ./dist
|
||||
COPY --from=builder /zipline/.next ./.next
|
||||
COPY --from=builder /zipline/package.json ./package.json
|
||||
|
||||
COPY --from=builder /zipline/node_modules ./node_modules
|
||||
COPY --from=builder /zipline/node_modules/.prisma/client ./node_modules/.prisma/client
|
||||
COPY --from=builder /zipline/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
|
||||
# Copy Startup Script
|
||||
COPY docker-entrypoint.sh /zipline
|
||||
# Make Startup Script Executable
|
||||
RUN chmod a+x /zipline/docker-entrypoint.sh
|
||||
# Set the entrypoint to the startup script
|
||||
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 dicedtomato
|
||||
Copyright (c) 2022 dicedtomato
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
# TypeX
|
||||
|
||||
A TypeScript based Image/File uploading server. Fast and Elegant.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
1. [Node](#node--typescript)
|
||||
2. [Common Databases](#common-databases)
|
||||
3. [Installing Database Drivers](#installing-database-drivers)
|
||||
1. [PostgreSQL](#getting-postgresql-drivers)
|
||||
2. [CockroachDB](#getting-cockroachdb-drivers)
|
||||
3. [MySQL](#getting-mysql-drivers)
|
||||
4. [MariaDB](#getting-mariadb-drivers)
|
||||
5. [Microsoft SQL Server](#getting-microsoft-sql-drivers)
|
||||
2. [Updating TypeX](#updating-typex)
|
||||
3. [Installation](#installation)
|
||||
1. [Get the Source](#get-the-source--install-dependencies)
|
||||
2. [Setting up configurations](#configuration-options)
|
||||
1. [Upload Size](#upload)
|
||||
2. [User Settings](#user-settings)
|
||||
3. [Site Settings](#site-settings)
|
||||
4. [SSL Settings](#site-ssl-settings)
|
||||
5. [Administrator user](#administrator-user)
|
||||
6. [Database configuration](#database-configuration)
|
||||
7. [Session Secret](#session-secret)
|
||||
8. [Particles.JS](#meta-configuration)
|
||||
3. [Example Config](#example-config)
|
||||
4. [Compiling Source](#compiling-typescript-for-running)
|
||||
5. [Running Compiled Source](#running-compiled-source)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Dependencies needed for running TypeX
|
||||
|
||||
### Node & Typescript
|
||||
|
||||
Node.JS is what runs the show, and you will need it before anything else. Install it [here](https://nodejs.org)
|
||||
|
||||
Once Node is installed install Typescript by doing
|
||||
|
||||
```sh
|
||||
npm i typescript -g
|
||||
```
|
||||
|
||||
Verify your installation by running these commands
|
||||
|
||||
```sh
|
||||
tsc -v
|
||||
npm -v
|
||||
node -v
|
||||
```
|
||||
|
||||
They should all output something along the lines of
|
||||
|
||||
```sh
|
||||
-> tsc -v
|
||||
Version 3.8.3
|
||||
-> npm -v
|
||||
node 6.14.4
|
||||
-> node -v
|
||||
v13.13.0
|
||||
```
|
||||
|
||||
### Common Databases
|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/ "PostgresSQL")
|
||||
- [CockroachDB](https://www.cockroachlabs.com/ "Cockroach Labs")
|
||||
- [MySQL](https://www.mysql.com/ "MySQL")
|
||||
- [MariaDB](https://www.mariadb.com/ "MariaDB")
|
||||
- [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/ "Microsoft SQL Server")
|
||||
- [MongoDB](https://www.mongodb.com/ "MongoDB") (Coming soon!)
|
||||
|
||||
(check out [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md) for all types, you will need to use a different ORM config later on, view [this](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md#common-connection-options) for every option, more on this on Database configuration setup step)
|
||||
|
||||
### Installing Database Drivers
|
||||
|
||||
In this installation step, you will be installing the drivers of your choice database.
|
||||
|
||||
#### Getting PostgreSQL Drivers
|
||||
|
||||
Run the following command in order to get PostgreSQL drivers
|
||||
|
||||
```sh
|
||||
npm i pg --save-dev
|
||||
```
|
||||
|
||||
#### Getting CockroachDB Drivers
|
||||
|
||||
Run the following command in order to get CockroachDB drivers
|
||||
|
||||
```sh
|
||||
npm i cockroachdb --save-dev
|
||||
```
|
||||
|
||||
#### Getting MySQL Drivers
|
||||
|
||||
Run the following command in order to get MySQL drivers
|
||||
|
||||
```sh
|
||||
npm i mysql --save-dev
|
||||
```
|
||||
|
||||
#### Getting MariaDB Drivers
|
||||
|
||||
Run the following command in order to get MariaDB drivers
|
||||
|
||||
```sh
|
||||
npm i mariadb --save-dev
|
||||
```
|
||||
|
||||
#### Getting Microsoft SQL Drivers
|
||||
|
||||
Run the following command in order to get Microsoft SQL drivers
|
||||
|
||||
```sh
|
||||
npm i mssql --save-dev
|
||||
```
|
||||
|
||||
## Updating TypeX
|
||||
|
||||
Updating TypeX, is very simple. You can use this one-liner to update and compile code.
|
||||
|
||||
1. Run the following in the `typex` directory, if there were config changes, you should change them before this command.
|
||||
|
||||
```sh
|
||||
git pull && tsc -p .
|
||||
```
|
||||
|
||||
2. After that, you just need to restart the process for changes to take effect.
|
||||
|
||||
## Installation
|
||||
|
||||
Now that you have considered what prerequisites you would like, lets actually install this! This installation is based on Linux systems, yet will work on both MacOSX and Windows with their respective commands
|
||||
|
||||
### Get the Source & Install Dependencies
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dicedtomatoreal/typex
|
||||
cd typex
|
||||
tsc -p .
|
||||
npm start
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
Every single configuration option will be listed here
|
||||
|
||||
#### Upload
|
||||
|
||||
**Config Property:** `upload`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| ------------------- | ------- | ------------------------------------------------------------ |
|
||||
| `upload.fileLength` | integer | how long the random id for a file should be |
|
||||
| `upload.tempDir` | string | temporary directory, files are stored here and then deleted. |
|
||||
| `upload.uploadDir` | string | upload directory (where all uploads are stored) |
|
||||
| `upload.route` | string | Route for uploads, default is /u, ex.`/u/hd27ua.png` |
|
||||
|
||||
#### User Settings
|
||||
|
||||
**Config Property:** `user`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| ------------------ | ------- | ---------------------------------------------------- |
|
||||
| `user.tokenLength` | integer | How long the randomly generated user token should be |
|
||||
|
||||
#### Site Settings
|
||||
|
||||
**Config Property:** `site`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `site.protocol` | integer | protocol (http or https) |
|
||||
| `site.serveHTTP` | string | Port to run the web server on with HTTP (can be used with nginx + CloudFlare as a reverse proxy and let CloudFlare take care of SSL) |
|
||||
| `site.serveHTTPS` | string | Port to run the web server on with HTTPS (only will be used if `site.protocol` is `https`) (you will need SSL certificates! See [this](#site-ssl-settings)) |
|
||||
| `site.logRoutes` | boolean | Wether or not to log routes when they are requested |
|
||||
|
||||
#### Site SSL Settings
|
||||
|
||||
**Config Property:** `site.ssl`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| --------------- | ------ | ----------------------------------------------- |
|
||||
| `site.ssl.key` | string | path to ssl private key. ex: `./ssl/server.key` |
|
||||
| `site.ssl.cert` | string | path to ssl certificate. ex: `./ssl/cert.crt` |
|
||||
|
||||
#### Administrator User
|
||||
|
||||
**Config Property:** `administrator`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| ------------------------ | ------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `administrator.password` | string | password of administrator user (NOT RECOMENDED to use administrator user, set this to a SECURE password) |
|
||||
|
||||
#### Database Configuration
|
||||
|
||||
**Config Property:** `orm`
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| ----------------- | -------- | --------------------------------------------------------------------------------- |
|
||||
| `orm.type` | string | `mariadb`, `mysql`, `postgres`, `cockroach`, `mssql` |
|
||||
| `orm.host` | string | `localhost` or different IP |
|
||||
| `orm.port` | integer | `5432` or different pot |
|
||||
| `orm.username` | string | username |
|
||||
| `orm.password` | string | password |
|
||||
| `orm.database` | string | database to use |
|
||||
| `orm.synchronize` | boolean | synchronize database to database, or not |
|
||||
| `orm.logging` | boolean | log all queries |
|
||||
| `orm.entities` | string[] | entity paths (should not be edited, and should be `["out/src/entities/**/*.js"]`) |
|
||||
|
||||
#### Session Secret
|
||||
|
||||
**Config Property:** `sessionSecret`
|
||||
|
||||
A Random string of characters (anything)
|
||||
|
||||
#### Session Secret
|
||||
|
||||
**Config Property:** `saltRounds`
|
||||
|
||||
The ammount of salt rounds needed to salt a password! (used for password encryption)
|
||||
|
||||
#### Meta Configuration
|
||||
|
||||
**Config Property:** `meta`
|
||||
|
||||
Particles.JS, can be enabled and it's config can be changed willingly.
|
||||
|
||||
| Config Property | Type | Description / Expected Values |
|
||||
| --------------- | ------ | -------------------------------------------------------------------------------------------- |
|
||||
| `meta.favicon` | string | has to be in /public/assets folder and should be formatted as `"/public/assets/<file name>"` |
|
||||
| `meta.title` | string | title of your server shows up like `<title> - Login` or `<title> - Dashboard` |
|
||||
|
||||
### Example Config
|
||||
|
||||
```json
|
||||
{
|
||||
"upload": {
|
||||
"fileLength": 6,
|
||||
"tempDir": "./temp",
|
||||
"uploadDir": "./uploads",
|
||||
"route": "/u"
|
||||
},
|
||||
"shorten": {
|
||||
"idLength": 4,
|
||||
"route": "/s"
|
||||
},
|
||||
"user": {
|
||||
"tokenLength": 32
|
||||
},
|
||||
"site": {
|
||||
"protocol": "http",
|
||||
"returnProtocol": "https",
|
||||
"ssl": {
|
||||
"key": "./ssl/server.key",
|
||||
"cert": "./ssl/server.crt"
|
||||
},
|
||||
"serveHTTPS": 8000,
|
||||
"serveHTTP": 443,
|
||||
"logRoutes": true
|
||||
},
|
||||
"administrator": {
|
||||
"password": "1234"
|
||||
},
|
||||
"orm": {
|
||||
"type": "postgres",
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"username": "user",
|
||||
"password": "1234",
|
||||
"database": "typex",
|
||||
"synchronize": true,
|
||||
"logging": false,
|
||||
"entities": ["out/src/entities/**/*.js"]
|
||||
},
|
||||
"sessionSecret": "1234",
|
||||
"saltRounds": 10, // You might get an error if its over a certain number, so choose carefully.
|
||||
"meta": {
|
||||
"favicon": "/public/assets/typex_small_circle.png",
|
||||
"title": "TypeX"
|
||||
},
|
||||
"discordWebhook": {
|
||||
"enabled": true,
|
||||
"url": "https://canary.discordapp.com/api/webhooks/id/token",
|
||||
"username": "TypeX Logs",
|
||||
"avatarURL": "https://domain/public/assets/typex_small_circle.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Compiling Typescript for running
|
||||
|
||||
Compile the Typescript code before running the code, or you can run it with `ts-node` which is not recommended. **_MAKE SURE YOU ARE IN THE PROJECT DIR!_**
|
||||
|
||||
```sh
|
||||
tsc -p .
|
||||
```
|
||||
|
||||
### Running Compiled Source
|
||||
|
||||
Run the webserver by running
|
||||
|
||||
```sh
|
||||
node out/src
|
||||
```
|
||||
|
||||
## How you can upload
|
||||
|
||||
These are the options you must pass when uploading a url or image/file
|
||||
|
||||
### Uploader
|
||||
|
||||
| Property | Value |
|
||||
| --------- | ----------------------------------- |
|
||||
| URL | `https://<DOMAIN>/api/upload` |
|
||||
| Header | `authorization: <TOKEN>` |
|
||||
| Header | `content-type: multipart/form-data` |
|
||||
| File name | `file` |
|
||||
|
||||
### URL Shortener
|
||||
|
||||
| Property | Value |
|
||||
| -------- | -------------------------------- |
|
||||
| URL | `https://<DOMAIN>/api/shorten` |
|
||||
| Header | `authorization: <TOKEN>` |
|
||||
| Header | `content-type: application/json` |
|
||||
| Data | `{"url": "<URL>"}` |
|
||||
@@ -1,21 +1,159 @@
|
||||
# Zipline
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||
Zipline is a Typescript based image/file uploading service & URL shortening service! Simple, and elegant.
|
||||
A ShareX/file upload server that is easy to use, packed with features, and with an easy setup!
|
||||
|
||||
# Images
|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk)
|
||||
[](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest)
|
||||
|
||||
# Bugs?
|
||||
</div>
|
||||
|
||||
Make sure to open an issue if you think you ran into an issue, or need help with anything.
|
||||
## Features
|
||||
|
||||
# Where is all the old information here?
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Image uploading
|
||||
- Image compression
|
||||
- Password Protected Uploads
|
||||
- URL shortening
|
||||
- Text uploading
|
||||
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
|
||||
- Discord embeds (OG metadata)
|
||||
- Gallery viewer, and multiple file format support
|
||||
- Code highlighting
|
||||
- Fully customizable Discord webhook notifications
|
||||
- OAuth2 registration (Discord and GitHub)
|
||||
- Two-Factor authentication with Google Authenticator, Authy, etc (totp services).
|
||||
- User invites
|
||||
- File Chunking (for large files)
|
||||
- File deletion once it reaches a certain amount of views
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
|
||||
|
||||
All configuration options have been moved to the wiki, as the README was getting too long and hard to manage.
|
||||
<details>
|
||||
<summary><h2>Screenshots (click)</h2></summary>
|
||||
|
||||
View full album at [imgur](https://imgur.com/a/GzyowZ7)
|
||||
|
||||

|
||||

|
||||

|
||||
</details>
|
||||
|
||||
# Usage
|
||||
|
||||
## Install & run with Docker
|
||||
|
||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### After installing
|
||||
|
||||
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
|
||||
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
|
||||
|
||||
## Building & running from source
|
||||
|
||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
# npm install
|
||||
yarn install
|
||||
# npm run build
|
||||
yarn build
|
||||
# npm start
|
||||
yarn start
|
||||
```
|
||||
|
||||
# NGINX Proxy
|
||||
|
||||
This section requires [NGINX](https://nginx.org/).
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80 default_server;
|
||||
client_max_body_size 100M;
|
||||
server_name <your domain (optional)>;
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Website
|
||||
|
||||
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
|
||||
|
||||
# ShareX (Windows)
|
||||
|
||||
This section requires [ShareX](https://www.getsharex.com/).
|
||||
|
||||
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex)
|
||||
|
||||
# Flameshot (Linux)
|
||||
|
||||
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
|
||||
|
||||
<details>
|
||||
<summary>Wayland instructions</summary>
|
||||
|
||||
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
|
||||
|
||||
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
|
||||
|
||||
After this, replace the `xsel -ib` with `wl-copy` in the script.
|
||||
|
||||
</details>
|
||||
|
||||
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
|
||||
To upload files using flameshot we will use a script. Replace $TOKEN and $HOST with your own values, you probably know how to do this if you use linux.
|
||||
|
||||
```shell
|
||||
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
|
||||
flameshot gui -r > ~/Pictures/$DATE;
|
||||
|
||||
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r 'files[0].url' | xsel -ib
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
## Bug reports
|
||||
|
||||
Create an issue on GitHub and use the template, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||
|
||||
- The steps to reproduce the bug
|
||||
- Logs of Zipline
|
||||
- The version of Zipline
|
||||
- Your OS & Browser including server OS
|
||||
- What you were expecting to see
|
||||
|
||||
## Feature requests
|
||||
|
||||
Create a discussion on GitHub, please include the following:
|
||||
|
||||
- Brief explanation of the feature in the title (very brief please)
|
||||
- How it would work (Be detailed!)
|
||||
|
||||
## Pull Requests (contributions to the codebase)
|
||||
|
||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.6.x | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.
|
||||
@@ -0,0 +1,32 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- .env.local
|
||||
volumes:
|
||||
- '$PWD/uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
@@ -0,0 +1,37 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CORE_RETURN_HTTPS=false
|
||||
- CORE_SECRET=changethis
|
||||
- CORE_HOST=0.0.0.0
|
||||
- CORE_PORT=3000
|
||||
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
||||
- CORE_LOGGER=true
|
||||
volumes:
|
||||
- './uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
node --enable-source-maps dist/index.js
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
**/
|
||||
module.exports = {
|
||||
images: {
|
||||
domains: [
|
||||
// For sharex icon in manage user
|
||||
'getsharex.com',
|
||||
// For flameshot icon, and maybe in the future other stuff from github
|
||||
'raw.githubusercontent.com',
|
||||
// Google Icon
|
||||
'madeby.google.com',
|
||||
],
|
||||
},
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
@@ -1,36 +1,98 @@
|
||||
{
|
||||
"name": "typex",
|
||||
"version": "2.1.4",
|
||||
"name": "zipline",
|
||||
"version": "3.7.0-rc4",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"start": "node out/src"
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
"dev:run": "cross-env DEBUG=true REACT_EDITOR=code NODE_ENV=development RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false node --enable-source-maps dist",
|
||||
"build": "npm-run-all build:server build:schema build:next",
|
||||
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:server build:schema build:next",
|
||||
"build:server": "tsup",
|
||||
"build:next": "next build",
|
||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
|
||||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node dist",
|
||||
"lint": "next lint",
|
||||
"docker:up": "docker-compose up",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
||||
"docker:up-dev": "docker-compose --file docker-compose.dev.yml up",
|
||||
"docker:down-dev": "docker-compose --file docker-compose.dev.yml down",
|
||||
"scripts:read-config": "node --enable-source-maps dist/scripts/read-config",
|
||||
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
|
||||
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
|
||||
"scripts:set-user": "node --enable-source-maps dist/scripts/set-user",
|
||||
"scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte",
|
||||
"scripts:query-size": "node --enable-source-maps dist/scripts/query-size"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ayanaware/logger": "^2.2.1",
|
||||
"@overnightjs/core": "^1.6.15",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/centra": "^2.2.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/mime": "^2.0.1",
|
||||
"@types/multer": "^1.4.3",
|
||||
"@types/semver": "^7.3.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"centra": "^2.4.2",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"ejs": "^3.0.2",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"http-status-codes": "^1.4.0",
|
||||
"mime": "^2.4.4",
|
||||
"multer": "^1.4.2",
|
||||
"semver": "^7.3.2",
|
||||
"spectre.css": "^0.5.8",
|
||||
"typeorm": "^0.2.24"
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/dropzone": "^5.10.5",
|
||||
"@mantine/form": "^5.10.5",
|
||||
"@mantine/hooks": "^5.10.5",
|
||||
"@mantine/modals": "^5.10.5",
|
||||
"@mantine/next": "^5.10.5",
|
||||
"@mantine/notifications": "^5.10.5",
|
||||
"@mantine/prism": "^5.10.5",
|
||||
"@prisma/client": "^4.10.1",
|
||||
"@prisma/internals": "^4.10.1",
|
||||
"@prisma/migrate": "^4.10.1",
|
||||
"@sapphire/shapeshift": "^3.8.1",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"argon2": "^0.30.3",
|
||||
"cookie": "^0.5.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv-expand": "^10.0.0",
|
||||
"exiftool-vendored": "^21.2.0",
|
||||
"fastify": "^4.13.0",
|
||||
"fastify-plugin": "^4.5.0",
|
||||
"fflate": "^0.7.4",
|
||||
"find-my-way": "^7.5.0",
|
||||
"katex": "^0.16.4",
|
||||
"mantine-datatable": "^1.8.6",
|
||||
"minio": "^7.0.32",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^13.2.1",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^4.10.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-markdown": "^8.0.5",
|
||||
"recharts": "^2.4.3",
|
||||
"recoil": "^0.7.6",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mongodb": "^3.5.8",
|
||||
"pg": "^8.0.3",
|
||||
"prettier": "2.1.2"
|
||||
}
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/minio": "^7.0.16",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.4",
|
||||
"tsup": "^6.6.3",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
},
|
||||
"packageManager": "yarn@3.3.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"administrator" BOOLEAN NOT NULL DEFAULT false,
|
||||
"embedTitle" TEXT,
|
||||
"embedColor" TEXT NOT NULL DEFAULT E'#2f3136',
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Image" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"file" TEXT NOT NULL,
|
||||
"mimetype" TEXT NOT NULL DEFAULT E'image/png',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleImage" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"invis" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"to" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"invis" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage.invis_unique" ON "InvisibleImage"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage_id_unique" ON "InvisibleImage"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl.invis_unique" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_id_unique" ON "InvisibleUrl"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("id") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD FOREIGN KEY ("id") REFERENCES "Url"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "systemTheme" TEXT NOT NULL DEFAULT E'dark_blue';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Theme" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"primary" TEXT NOT NULL,
|
||||
"secondary" TEXT NOT NULL,
|
||||
"error" TEXT NOT NULL,
|
||||
"warning" TEXT NOT NULL,
|
||||
"info" TEXT NOT NULL,
|
||||
"border" TEXT NOT NULL,
|
||||
"mainBackground" TEXT NOT NULL,
|
||||
"paperBackground" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Theme_userId_unique" ON "Theme"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "favorite" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "InvisibleImage_id_unique";
|
||||
|
||||
-- AlterTable
|
||||
CREATE SEQUENCE "invisibleimage_id_seq";
|
||||
ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL,
|
||||
ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'),
|
||||
ADD PRIMARY KEY ("id");
|
||||
ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "embed" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `InvisibleUrl` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Url` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "InvisibleUrl";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Url";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD CONSTRAINT "Theme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage.invis_unique" RENAME TO "InvisibleImage_invis_key";
|
||||
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"invis" TEXT NOT NULL,
|
||||
"urlId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InvisibleUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_id_key" ON "Url"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_invis_key" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_urlId_unique" ON "InvisibleUrl"("urlId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "embedSiteName" TEXT DEFAULT E'{image.file} • {user.name}';
|
||||
@@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "ratelimited" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage_imageId_unique" RENAME TO "InvisibleImage_imageId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleUrl_urlId_unique" RENAME TO "InvisibleUrl_urlId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Theme_userId_unique" RENAME TO "Theme_userId_key";
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Stats" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Stats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ImageFormat" AS ENUM ('UUID', 'DATE', 'RANDOM');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "format" "ImageFormat" NOT NULL DEFAULT E'RANDOM';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ImageFormat" ADD VALUE 'NAME';
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
- You are about to drop the `Theme` table. If the table is not empty, all the data it contains will be lost.
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "systemTheme" SET DEFAULT E'system';
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Theme";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "domains" TEXT[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "password" TEXT;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `ratelimited` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "ratelimited",
|
||||
ADD COLUMN "ratelimit" TIMESTAMP(3);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Invite" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"used" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdById" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Invite" ALTER COLUMN "expires_at" DROP NOT NULL,
|
||||
ALTER COLUMN "expires_at" DROP DEFAULT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "expires_at" TIMESTAMP(3);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "oauth" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "oauthProvider" TEXT,
|
||||
ALTER COLUMN "password" DROP NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "oauthAccessToken" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "superAdmin" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "maxViews" INTEGER;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "maxViews" INTEGER;
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `oauth` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `oauthAccessToken` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `oauthProvider` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OauthProviders" AS ENUM ('DISCORD', 'GITHUB');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "oauth",
|
||||
DROP COLUMN "oauthAccessToken",
|
||||
DROP COLUMN "oauthProvider";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OAuth" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"provider" "OauthProviders" NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OAuth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OAuth_provider_key" ON "OAuth"("provider");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "OAuth_provider_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OAuth" ADD COLUMN "refresh" TEXT;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "OauthProviders" ADD VALUE 'GOOGLE';
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `username` to the `OAuth` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OAuth" ADD COLUMN "username" TEXT NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "totpSecret" TEXT;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_urlId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Invite" DROP CONSTRAINT "Invite_createdById_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[provider,oauthId]` on the table `OAuth` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `oauthId` to the `OAuth` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "OAuth" ADD COLUMN "oauthId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OAuth_provider_oauthId_key" ON "OAuth"("provider", "oauthId");
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `embedColor` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `embedSiteName` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `embedTitle` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "embedColor",
|
||||
DROP COLUMN "embedSiteName",
|
||||
DROP COLUMN "embedTitle",
|
||||
ADD COLUMN "embed" JSONB NOT NULL DEFAULT '{}';
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" RENAME COLUMN "created_at" TO "createdAt";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" RENAME COLUMN "expires_at" TO "expiresAt";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" RENAME COLUMN "file" TO "name";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" RENAME COLUMN "created_at" TO "createdAt";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Stats" RENAME COLUMN "created_at" TO "createdAt";
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Invite" RENAME COLUMN "created_at" TO "createdAt";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Invite" RENAME COLUMN "expires_at" TO "expiresAt";
|
||||
@@ -0,0 +1,19 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ImageFormat" RENAME TO "FileNameFormat";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" RENAME TO "File";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "InvisibleImage" RENAME TO "InvisibleFile";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "InvisibleFile" RENAME COLUMN "imageId" TO "fileId";
|
||||
|
||||
-- AlterForeignKey
|
||||
ALTER TABLE "InvisibleFile" RENAME CONSTRAINT "InvisibleImage_imageId_fkey" TO "InvisibleFile_fileId_fkey";
|
||||
ALTER INDEX "InvisibleImage_imageId_key" RENAME TO "InvisibleFile_fileId_key";
|
||||
|
||||
-- AlterForeignKey
|
||||
ALTER TABLE "InvisibleFile" RENAME CONSTRAINT "InvisibleImage_pkey" TO "InvisibleFile_pkey";
|
||||
ALTER TABLE "File" RENAME CONSTRAINT "Image_pkey" TO "File_pkey";
|
||||
@@ -0,0 +1,8 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "File" ADD COLUMN "originalName" TEXT;
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "File" RENAME CONSTRAINT "Image_userId_fkey" TO "File_userId_fkey";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage_invis_key" RENAME TO "InvisibleFile_invis_key";
|
||||
@@ -0,0 +1,19 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "File" ADD COLUMN "folderId" INTEGER;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Folder" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Folder" ADD COLUMN "public" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "File" ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,136 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String?
|
||||
avatar String?
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
superAdmin Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
embed Json @default("{}")
|
||||
ratelimit DateTime?
|
||||
totpSecret String?
|
||||
domains String[]
|
||||
oauth OAuth[]
|
||||
files File[]
|
||||
urls Url[]
|
||||
Invite Invite[]
|
||||
Folder Folder[]
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
public Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
|
||||
files File[]
|
||||
}
|
||||
|
||||
enum FileNameFormat {
|
||||
UUID
|
||||
DATE
|
||||
RANDOM
|
||||
NAME
|
||||
}
|
||||
|
||||
model File {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
originalName String?
|
||||
mimetype String @default("image/png")
|
||||
createdAt DateTime @default(now())
|
||||
size Int @default(0)
|
||||
expiresAt DateTime?
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
password String?
|
||||
invisible InvisibleFile?
|
||||
format FileNameFormat @default(RANDOM)
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
userId Int?
|
||||
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId Int?
|
||||
}
|
||||
|
||||
model InvisibleFile {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
|
||||
fileId Int @unique
|
||||
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Url {
|
||||
id String @id @unique
|
||||
destination String
|
||||
vanity String?
|
||||
createdAt DateTime @default(now())
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
userId Int?
|
||||
}
|
||||
|
||||
model InvisibleUrl {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
|
||||
urlId String @unique
|
||||
url Url @relation(fields: [urlId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Stats {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
data Json
|
||||
}
|
||||
|
||||
model Invite {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
used Boolean @default(false)
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
||||
createdById Int
|
||||
}
|
||||
|
||||
model OAuth {
|
||||
id Int @id @default(autoincrement())
|
||||
provider OauthProviders
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int
|
||||
username String
|
||||
oauthId String?
|
||||
token String
|
||||
refresh String?
|
||||
|
||||
@@unique([provider, oauthId])
|
||||
}
|
||||
|
||||
enum OauthProviders {
|
||||
DISCORD
|
||||
GITHUB
|
||||
GOOGLE
|
||||
}
|
||||
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 119 KiB |
@@ -1,597 +0,0 @@
|
||||
/*! Spectre.css Icons v0.5.8 | MIT License | github.com/picturepan2/spectre */
|
||||
.icon {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
font-style: normal;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
vertical-align: middle;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.icon::before,
|
||||
.icon::after {
|
||||
content: "";
|
||||
display: block;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.icon.icon-2x {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.icon.icon-3x {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
.icon.icon-4x {
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
|
||||
.accordion .icon,
|
||||
.btn .icon,
|
||||
.toast .icon,
|
||||
.menu .icon {
|
||||
vertical-align: -10%;
|
||||
}
|
||||
|
||||
.btn-lg .icon {
|
||||
vertical-align: -15%;
|
||||
}
|
||||
|
||||
.icon-arrow-down::before,
|
||||
.icon-arrow-left::before,
|
||||
.icon-arrow-right::before,
|
||||
.icon-arrow-up::before,
|
||||
.icon-downward::before,
|
||||
.icon-back::before,
|
||||
.icon-forward::before,
|
||||
.icon-upward::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-right: 0;
|
||||
height: .65em;
|
||||
width: .65em;
|
||||
}
|
||||
|
||||
.icon-arrow-down::before {
|
||||
transform: translate(-50%, -75%) rotate(225deg);
|
||||
}
|
||||
|
||||
.icon-arrow-left::before {
|
||||
transform: translate(-25%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-arrow-right::before {
|
||||
transform: translate(-75%, -50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.icon-arrow-up::before {
|
||||
transform: translate(-50%, -25%) rotate(45deg);
|
||||
}
|
||||
|
||||
.icon-back::after,
|
||||
.icon-forward::after {
|
||||
background: currentColor;
|
||||
height: .1rem;
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-downward::after,
|
||||
.icon-upward::after {
|
||||
background: currentColor;
|
||||
height: .8em;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-back::after {
|
||||
left: 55%;
|
||||
}
|
||||
|
||||
.icon-back::before {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-downward::after {
|
||||
top: 45%;
|
||||
}
|
||||
|
||||
.icon-downward::before {
|
||||
transform: translate(-50%, -50%) rotate(-135deg);
|
||||
}
|
||||
|
||||
.icon-forward::after {
|
||||
left: 45%;
|
||||
}
|
||||
|
||||
.icon-forward::before {
|
||||
transform: translate(-50%, -50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.icon-upward::after {
|
||||
top: 55%;
|
||||
}
|
||||
|
||||
.icon-upward::before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.icon-caret::before {
|
||||
border-left: .3em solid transparent;
|
||||
border-right: .3em solid transparent;
|
||||
border-top: .3em solid currentColor;
|
||||
height: 0;
|
||||
transform: translate(-50%, -25%);
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.icon-menu::before {
|
||||
background: currentColor;
|
||||
box-shadow: 0 -.35em, 0 .35em;
|
||||
height: .1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-apps::before {
|
||||
background: currentColor;
|
||||
box-shadow: -.35em -.35em, -.35em 0, -.35em .35em, 0 -.35em, 0 .35em, .35em -.35em, .35em 0, .35em .35em;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.icon-resize-horiz::before,
|
||||
.icon-resize-horiz::after,
|
||||
.icon-resize-vert::before,
|
||||
.icon-resize-vert::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-right: 0;
|
||||
height: .45em;
|
||||
width: .45em;
|
||||
}
|
||||
|
||||
.icon-resize-horiz::before,
|
||||
.icon-resize-vert::before {
|
||||
transform: translate(-50%, -90%) rotate(45deg);
|
||||
}
|
||||
|
||||
.icon-resize-horiz::after,
|
||||
.icon-resize-vert::after {
|
||||
transform: translate(-50%, -10%) rotate(225deg);
|
||||
}
|
||||
|
||||
.icon-resize-horiz::before {
|
||||
transform: translate(-90%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-resize-horiz::after {
|
||||
transform: translate(-10%, -50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.icon-more-horiz::before,
|
||||
.icon-more-vert::before {
|
||||
background: currentColor;
|
||||
border-radius: 50%;
|
||||
box-shadow: -.4em 0, .4em 0;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.icon-more-vert::before {
|
||||
box-shadow: 0 -.4em, 0 .4em;
|
||||
}
|
||||
|
||||
.icon-plus::before,
|
||||
.icon-minus::before,
|
||||
.icon-cross::before {
|
||||
background: currentColor;
|
||||
height: .1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-plus::after,
|
||||
.icon-cross::after {
|
||||
background: currentColor;
|
||||
height: 100%;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-cross::before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-cross::after {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-cross::before,
|
||||
.icon-cross::after {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.icon-check::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
height: .5em;
|
||||
transform: translate(-50%, -75%) rotate(-45deg);
|
||||
width: .9em;
|
||||
}
|
||||
|
||||
.icon-stop {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.icon-stop::before {
|
||||
background: currentColor;
|
||||
height: .1rem;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.icon-shutdown {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.icon-shutdown::before {
|
||||
background: currentColor;
|
||||
content: "";
|
||||
height: .5em;
|
||||
top: .1em;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-refresh::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
border-right-color: transparent;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.icon-refresh::after {
|
||||
border: .2em solid currentColor;
|
||||
border-left-color: transparent;
|
||||
border-top-color: transparent;
|
||||
height: 0;
|
||||
left: 80%;
|
||||
top: 20%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.icon-search::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
height: .75em;
|
||||
left: 5%;
|
||||
top: 5%;
|
||||
transform: translate(0, 0) rotate(45deg);
|
||||
width: .75em;
|
||||
}
|
||||
|
||||
.icon-search::after {
|
||||
background: currentColor;
|
||||
height: .1rem;
|
||||
left: 80%;
|
||||
top: 80%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: .4em;
|
||||
}
|
||||
|
||||
.icon-edit::before {
|
||||
border: .1rem solid currentColor;
|
||||
height: .4em;
|
||||
transform: translate(-40%, -60%) rotate(-45deg);
|
||||
width: .85em;
|
||||
}
|
||||
|
||||
.icon-edit::after {
|
||||
border: .15em solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
height: 0;
|
||||
left: 5%;
|
||||
top: 95%;
|
||||
transform: translate(0, -100%);
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.icon-delete::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom-left-radius: .1rem;
|
||||
border-bottom-right-radius: .1rem;
|
||||
border-top: 0;
|
||||
height: .75em;
|
||||
top: 60%;
|
||||
width: .75em;
|
||||
}
|
||||
|
||||
.icon-delete::after {
|
||||
background: currentColor;
|
||||
box-shadow: -.25em .2em, .25em .2em;
|
||||
height: .1rem;
|
||||
top: .05rem;
|
||||
width: .5em;
|
||||
}
|
||||
|
||||
.icon-share {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: .1rem;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.icon-share::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
height: .4em;
|
||||
left: 100%;
|
||||
top: .25em;
|
||||
transform: translate(-125%, -50%) rotate(-45deg);
|
||||
width: .4em;
|
||||
}
|
||||
|
||||
.icon-share::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-radius: 75% 0;
|
||||
border-right: 0;
|
||||
height: .5em;
|
||||
width: .6em;
|
||||
}
|
||||
|
||||
.icon-flag::before {
|
||||
background: currentColor;
|
||||
height: 1em;
|
||||
left: 15%;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-flag::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom-right-radius: .1rem;
|
||||
border-left: 0;
|
||||
border-top-right-radius: .1rem;
|
||||
height: .65em;
|
||||
left: 60%;
|
||||
top: 35%;
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-bookmark::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: .1rem;
|
||||
border-top-right-radius: .1rem;
|
||||
height: .9em;
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-bookmark::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-radius: .1rem;
|
||||
height: .5em;
|
||||
transform: translate(-50%, 35%) rotate(-45deg) skew(15deg, 15deg);
|
||||
width: .5em;
|
||||
}
|
||||
|
||||
.icon-download,
|
||||
.icon-upload {
|
||||
border-bottom: .1rem solid currentColor;
|
||||
}
|
||||
|
||||
.icon-download::before,
|
||||
.icon-upload::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-right: 0;
|
||||
height: .5em;
|
||||
transform: translate(-50%, -60%) rotate(-135deg);
|
||||
width: .5em;
|
||||
}
|
||||
|
||||
.icon-download::after,
|
||||
.icon-upload::after {
|
||||
background: currentColor;
|
||||
height: .6em;
|
||||
top: 40%;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-upload::before {
|
||||
transform: translate(-50%, -60%) rotate(45deg);
|
||||
}
|
||||
|
||||
.icon-upload::after {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.icon-copy::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-radius: .1rem;
|
||||
border-right: 0;
|
||||
height: .8em;
|
||||
left: 40%;
|
||||
top: 35%;
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-copy::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: .1rem;
|
||||
height: .8em;
|
||||
left: 60%;
|
||||
top: 60%;
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-time {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.icon-time::before {
|
||||
background: currentColor;
|
||||
height: .4em;
|
||||
transform: translate(-50%, -75%);
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-time::after {
|
||||
background: currentColor;
|
||||
height: .3em;
|
||||
transform: translate(-50%, -75%) rotate(90deg);
|
||||
transform-origin: 50% 90%;
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-mail::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: .1rem;
|
||||
height: .8em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.icon-mail::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
height: .5em;
|
||||
transform: translate(-50%, -90%) rotate(-45deg) skew(10deg, 10deg);
|
||||
width: .5em;
|
||||
}
|
||||
|
||||
.icon-people::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
height: .45em;
|
||||
top: 25%;
|
||||
width: .45em;
|
||||
}
|
||||
|
||||
.icon-people::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50% 50% 0 0;
|
||||
height: .4em;
|
||||
top: 75%;
|
||||
width: .9em;
|
||||
}
|
||||
|
||||
.icon-message {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-radius: .1rem;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.icon-message::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom-right-radius: .1rem;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
height: .8em;
|
||||
left: 65%;
|
||||
top: 40%;
|
||||
width: .7em;
|
||||
}
|
||||
|
||||
.icon-message::after {
|
||||
background: currentColor;
|
||||
border-radius: .1rem;
|
||||
height: .3em;
|
||||
left: 10%;
|
||||
top: 100%;
|
||||
transform: translate(0, -90%) rotate(45deg);
|
||||
width: .1rem;
|
||||
}
|
||||
|
||||
.icon-photo {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
.icon-photo::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
height: .25em;
|
||||
left: 35%;
|
||||
top: 35%;
|
||||
width: .25em;
|
||||
}
|
||||
|
||||
.icon-photo::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
height: .5em;
|
||||
left: 60%;
|
||||
transform: translate(-50%, 25%) rotate(-45deg);
|
||||
width: .5em;
|
||||
}
|
||||
|
||||
.icon-link::before,
|
||||
.icon-link::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 5em 0 0 5em;
|
||||
border-right: 0;
|
||||
height: .5em;
|
||||
width: .75em;
|
||||
}
|
||||
|
||||
.icon-link::before {
|
||||
transform: translate(-70%, -45%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-link::after {
|
||||
transform: translate(-30%, -55%) rotate(135deg);
|
||||
}
|
||||
|
||||
.icon-location::before {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
height: .8em;
|
||||
transform: translate(-50%, -60%) rotate(-45deg);
|
||||
width: .8em;
|
||||
}
|
||||
|
||||
.icon-location::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
height: .2em;
|
||||
transform: translate(-50%, -80%);
|
||||
width: .2em;
|
||||
}
|
||||
|
||||
.icon-emoji {
|
||||
border: .1rem solid currentColor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.icon-emoji::before {
|
||||
border-radius: 50%;
|
||||
box-shadow: -.17em -.1em, .17em -.1em;
|
||||
height: .15em;
|
||||
width: .15em;
|
||||
}
|
||||
|
||||
.icon-emoji::after {
|
||||
border: .1rem solid currentColor;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
border-right-color: transparent;
|
||||
height: .5em;
|
||||
transform: translate(-50%, -40%) rotate(-135deg);
|
||||
width: .5em;
|
||||
}
|
||||
|
After Width: | Height: | Size: 279 KiB |
@@ -1,483 +0,0 @@
|
||||
const TypeX = {
|
||||
currentImagePage: 0,
|
||||
pagedNumbers: [],
|
||||
currentID: null
|
||||
}
|
||||
function whitespace(str) {
|
||||
return str === null || str.match(/^ *$/) !== null;
|
||||
}
|
||||
|
||||
function showAlert(type, message) {
|
||||
if (type === 'error') {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: message,
|
||||
footer: 'Try again later...'
|
||||
})
|
||||
} else if (type === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Success',
|
||||
text: message,
|
||||
footer: 'You did it!'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
var input = document.createElement('textarea');
|
||||
input.innerHTML = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
var result = document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function redoImageGrid(page, mode = null) {
|
||||
if (!page && !mode) return;
|
||||
document.getElementById('typexImages').innerHTML = '';
|
||||
let url = '';
|
||||
if (mode === 'prev') {
|
||||
if (TypeX.currentImagePage === 0) {
|
||||
url = '/api/images/user/pages?page=0';
|
||||
TypeX.currentImagePage = 0;
|
||||
} else {
|
||||
url = `/api/images/user/pages?page=${TypeX.currentImagePage - 1}`;
|
||||
TypeX.currentImagePage--;
|
||||
} //could be better :DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
|
||||
} else if (mode === 'next') {
|
||||
if (TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1] <= TypeX.currentImagePage + 1) {
|
||||
url = `/api/images/user/pages?page=${TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1]}`
|
||||
TypeX.currentImagePage = TypeX.pagedNumbers[TypeX.pagedNumbers.length - 1];
|
||||
} else {
|
||||
url = `/api/images/user/pages?page=${TypeX.currentImagePage + 1}`
|
||||
TypeX.currentImagePage++;
|
||||
}
|
||||
} else if (mode === 'normal') {
|
||||
url = `/api/images/user/pages?page=${page}`;
|
||||
TypeX.currentImagePage = Number(page);
|
||||
}
|
||||
$("#typexImagePaginationDropdown").val(TypeX.currentImagePage);
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error);
|
||||
try {
|
||||
json.page.forEach(image => {
|
||||
$('#typexImages').append(`
|
||||
<div class="column col-4">
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<img src="${image.url}" class="img-responsive" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.add('active')">
|
||||
<div class="modal" id="modal-image-mngt-${image.id}">
|
||||
<a href="#close" class="modal-overlay" aria-label="Close" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.remove('active')"></a>
|
||||
<div class="modal-container bg-dark">
|
||||
<div class="modal-header">
|
||||
<a href="#close" class="btn btn-clear float-right text-light" aria-label="Close" onclick="document.getElementById('modal-image-mngt-${image.id}').classList.remove('active')"></a>
|
||||
<div class="modal-title text-light h5">Manage Image ${image.id}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
This image is viewable at <a href="${image.url}">${image.url}</a> and has <b>${image.views}</b> views.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="button" onclick="deleteImage('${image.id}')">Delete Image</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
} catch (e) {
|
||||
document.getElementById('emptyImages').innerHTML = `
|
||||
<div class="empty bg-dark">
|
||||
<div class="empty-icon">
|
||||
<i class="icon icon-photo"></i>
|
||||
</div>
|
||||
<p class="empty-title h5">You have no imaages</p>
|
||||
<p class="empty-subtitle">Use the API to start uploading!</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('typexImagePagination').innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('updateImages').addEventListener('click', async () => {
|
||||
redoImageGrid('0', 'normal');
|
||||
document.getElementById('emptyImages').innerHTML = '';
|
||||
document.getElementById('typexImagePagination').innerHTML = '';
|
||||
|
||||
const resp = await fetch('/api/images/user/pages', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const json = await resp.json();
|
||||
try {
|
||||
$('#typexImagePagination').append(`
|
||||
<li class="page-item">
|
||||
<a class="page-link" aria-label="First" onclick="redoImageGrid('0', 'normal')">
|
||||
First
|
||||
</a>
|
||||
</li>`);
|
||||
$('#typexImagePagination').append(`
|
||||
<li class="page-item">
|
||||
<a class="page-link" aria-label="Previous" onclick="redoImageGrid(null, 'prev')">
|
||||
Prev
|
||||
</a>
|
||||
</li>`);
|
||||
$('#typexImagePagination').append(`
|
||||
<li class="page-item">
|
||||
<select class="form-select" id="typexImagePaginationDropdown">
|
||||
</select>
|
||||
</li>`)
|
||||
TypeX.pagedNumbers = json.pagedNums;
|
||||
json.pagedNums.forEach(p => {
|
||||
$('#typexImagePaginationDropdown').append(`
|
||||
<option onclick="redoImageGrid('${p}', 'normal')" value="${p}">${p + 1}</option>
|
||||
`)
|
||||
});
|
||||
$('#typexImagePagination').append(`
|
||||
<li class="page-item">
|
||||
<a onclick="redoImageGrid(null, 'next')">
|
||||
Next
|
||||
</a>
|
||||
</li>`);
|
||||
$('#typexImagePagination').append(`
|
||||
<li class="page-item">
|
||||
<a onclick="redoImageGrid(TypeX.pagedNumbers[TypeX.pagedNumbers.length-1], 'normal')">
|
||||
Last
|
||||
</a>
|
||||
</li>`);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateStatistics').addEventListener('click', async () => {
|
||||
const resp = await fetch('/api/images/statistics', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const json = await resp.json();
|
||||
try {
|
||||
document.getElementById('statsDescription').innerHTML = `You have an average of <b>${Math.floor(json.average).toLocaleString()} views</b> on your images, you have <b>${json.totalViews.toLocaleString()} views total</b>, you currently have <b>${json.images.toLocaleString()} images</b>!`
|
||||
document.getElementById('statsLeaderboardImages').innerHTML = '';
|
||||
document.getElementById('statsLeaderboardImageViews').innerHTML = '';
|
||||
for (let i = 0; i < json.table.images.length; i++) {
|
||||
const c = json.table.images[i];
|
||||
$('#statsLeaderboardImages').append(`
|
||||
<tr>
|
||||
<th>${i + 1}</th>
|
||||
<td>${c.username}</td>
|
||||
<td>${c.count.toLocaleString()}</td>
|
||||
</tr>
|
||||
`)
|
||||
}
|
||||
for (let i = 0; i < json.table.views.length; i++) {
|
||||
const c = json.table.views[i];
|
||||
$('#statsLeaderboardImageViews').append(`
|
||||
<tr>
|
||||
<th>${i + 1}</th>
|
||||
<td>${c.username}</td>
|
||||
<td>${c.count.toLocaleString()}</td>
|
||||
</tr>
|
||||
`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('updateShortens').addEventListener('click', async () => {
|
||||
const resp = await fetch('/api/shortens', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const json = await resp.json();
|
||||
try {
|
||||
document.getElementById('shortensTableShortens').innerHTML = '';
|
||||
for (const shorten of json) {
|
||||
$('#shortensTableShortens').append(`
|
||||
<tr>
|
||||
<th>${shorten.id}</th>
|
||||
<td><a href="${shorten.origin}">${shorten.origin}</a></td>
|
||||
<td><a href="${shorten.url}">${shorten.url}</a></td>
|
||||
</tr>
|
||||
`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const deleteImage = (id, url) => {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: `You are proceeding to delete image (${id}), you will not be able to recover it!`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it.'
|
||||
}).then(async (result) => {
|
||||
if (result.value) {
|
||||
try {
|
||||
const res = await fetch('/api/images/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error)
|
||||
else {
|
||||
Swal.fire(
|
||||
'Deleted!',
|
||||
`Deleted image (${id}) successfully.`,
|
||||
'success'
|
||||
);
|
||||
document.getElementById(`modal-image-mngt-${id}`).classList.remove('active')
|
||||
redoImageGrid('0', 'normal')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const deleteSpecificUser = (id, username) => {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: `You are proceeding to delete user ${username} (${id}), you will not be able to recover them!`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: `Yes, delete ${username}.`
|
||||
}).then(async (result) => {
|
||||
if (result.value) {
|
||||
try {
|
||||
const res = await fetch('/api/user/' + id, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error)
|
||||
else {
|
||||
Swal.fire(
|
||||
'Deleted!',
|
||||
`Deleted user ${username} (${id}) successfully.`,
|
||||
'success'
|
||||
);
|
||||
window.location.href = '/'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const saveUser = (id) => {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You are proceeding to edit your user.",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, save changes!'
|
||||
}).then(async (result) => {
|
||||
if (result.value) {
|
||||
const username = document.getElementById('usernameSave').value;
|
||||
const password = document.getElementById('passwordSave').value;
|
||||
if (whitespace(username)) return showAlert('error', 'Please input a username.')
|
||||
const res = await fetch(`/api/users/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
payload: 'USER_EDIT',
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error)
|
||||
else {
|
||||
Swal.fire(
|
||||
'Saved Changes!',
|
||||
'Changes were saved successfully!',
|
||||
'success'
|
||||
);
|
||||
window.location.href = '/'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function shortURL(token, url) {
|
||||
if (whitespace(url)) return showAlert('error', 'Please input a URL.')
|
||||
const res = await fetch('/api/shorten', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url
|
||||
})
|
||||
});
|
||||
try {
|
||||
let te = await res.text();
|
||||
Swal.fire(
|
||||
'URL Shortened!',
|
||||
`Shorten: <a target="_blank" href="${te}">${te}</a>`,
|
||||
'success'
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e.message.startsWith('Unexpected token < in JSON at position')) {
|
||||
let te = await res.text();
|
||||
Swal.fire(
|
||||
'URL Shortened!',
|
||||
`Shorten: <a target="_blank" href="${te}">${te}</a>`,
|
||||
'success'
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyToken = (token) => {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You are proceeding to copy your token, make sure NO ONE sees it.",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3`085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, copy it!'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
copyText(token);
|
||||
Swal.fire(
|
||||
'Copied!',
|
||||
'Your API Token has been copied.',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
function regenToken(id) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You are proceeding to regenerate your token, remember all apps using your current one will stop working.",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, regenerate it!'
|
||||
}).then(async (result) => {
|
||||
if (result.value) {
|
||||
const res = await fetch(`/api/users/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
payload: 'USER_TOKEN_RESET'
|
||||
})
|
||||
});
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error)
|
||||
else {
|
||||
Swal.fire(
|
||||
'Regenerated!',
|
||||
'Your API Token has been regenerated.',
|
||||
'success'
|
||||
);
|
||||
return window.location.href = '/'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
async function createUser() {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
if (whitespace(username)) return showAlert('error', 'Please input a username.')
|
||||
const res = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
administrator: document.getElementById('administrator').checked
|
||||
})
|
||||
});
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json.error || json.code) return showAlert('error', json.error)
|
||||
else {
|
||||
showAlert('success', `Created user ${json.username} (${json.id})`)
|
||||
return window.location.href = '/'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('addUser').addEventListener('click', async () => {
|
||||
if (document.getElementById('administrator').checked) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You are proceeding to create a user with administrator permissions, they can do whatever they want!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, create user!'
|
||||
}).then(async (result) => {
|
||||
if (result.value) {
|
||||
createUser()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createUser();
|
||||
}
|
||||
})
|
||||
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -1 +0,0 @@
|
||||
echo "Updating Zipline\n\n\n\n\n" && git pull && tsc -p .
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Card as MCard, Title } from '@mantine/core';
|
||||
|
||||
export default function Card({ name, children, ...other }) {
|
||||
return (
|
||||
<MCard p='md' shadow='sm' {...other}>
|
||||
{name && <Title order={2}>{name}</Title>}
|
||||
{children}
|
||||
</MCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createStyles, MantineSize, Textarea } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
|
||||
input: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: theme.fn.size({ size, sizes: theme.fontSizes }) - 2,
|
||||
height: '80vh',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CodeInput({ ...props }) {
|
||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (document.activeElement?.tagName !== 'TEXTAREA') return;
|
||||
|
||||
e.preventDefault();
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const start = target.selectionStart;
|
||||
const end = target.selectionEnd;
|
||||
target.value = `${target.value.substring(0, start)} ${target.value.substring(end)}`;
|
||||
target.selectionStart = target.selectionEnd = start + 2;
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Textarea classNames={{ input: classes.input }} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Card,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
HardDriveIcon,
|
||||
FileIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from './icons';
|
||||
import MutedText from './MutedText';
|
||||
import Type from './Type';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||
return other.tooltip ? (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Tooltip label={other.tooltip}>
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({
|
||||
image,
|
||||
disableMediaPreview,
|
||||
exifEnabled,
|
||||
refreshImages,
|
||||
reducedActions = false,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [overrideRender, setOverrideRender] = useState(false);
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const folders = useFolders();
|
||||
|
||||
const loading = deleteFile.isLoading || favoriteFile.isLoading;
|
||||
|
||||
const handleDelete = async () => {
|
||||
deleteFile.mutate(image.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setOpen(false);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
favoriteFile.mutate(
|
||||
{ id: image.id, favorite: !image.favorite },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const inFolder = image.folderId;
|
||||
|
||||
const refresh = () => {
|
||||
refreshImages();
|
||||
folders.refetch();
|
||||
};
|
||||
|
||||
const removeFromFolder = async () => {
|
||||
const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', {
|
||||
file: Number(image.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Removed from folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderMinusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to remove from folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addToFolder = async (t) => {
|
||||
const res = await useFetch('/api/user/folders/' + t, 'POST', {
|
||||
file: Number(image.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to add to folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (t) => {
|
||||
useFetch('/api/user/folders', 'POST', {
|
||||
name: t,
|
||||
add: [Number(image.id)],
|
||||
}).then((res) => {
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Created & added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { value: t, label: t };
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.name}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={image}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
disableMediaPreview={false}
|
||||
overrideRender={overrideRender}
|
||||
setOverrideRender={setOverrideRender}
|
||||
/>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
cols={3}
|
||||
breakpoints={[
|
||||
{ maxWidth: 600, cols: 1 },
|
||||
{ maxWidth: 900, cols: 2 },
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.name} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={HardDriveIcon} title='Size' subtitle={bytesToHuman(image.size || 0)} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
|
||||
{image.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={image?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(image.createdAt))}
|
||||
tooltip={new Date(image?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{image.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(image.expiresAt))}
|
||||
tooltip={new Date(image.expiresAt).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Group position='apart' my='md'>
|
||||
<Group position='left'>
|
||||
{exifEnabled && !reducedActions && (
|
||||
<Tooltip label='View Metadata'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/dashboard/metadata/${image.id}`, '_blank')}
|
||||
>
|
||||
<InfoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${
|
||||
folders.data.find((f) => f.id === image.folderId)?.name ?? ''
|
||||
}"`}
|
||||
>
|
||||
<ActionIcon
|
||||
color='red'
|
||||
variant='filled'
|
||||
onClick={removeFromFolder}
|
||||
loading={folders.isLoading}
|
||||
>
|
||||
<FolderMinusIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label='Add to folder'>
|
||||
<Select
|
||||
onChange={addToFolder}
|
||||
placeholder='Add to folder'
|
||||
data={[
|
||||
...(folders.data ? folders.data : []).map((folder) => ({
|
||||
value: String(folder.id),
|
||||
label: `${folder.id}: ${folder.name}`,
|
||||
})),
|
||||
]}
|
||||
searchable
|
||||
creatable
|
||||
getCreateLabel={(query) => `Create folder "${query}"`}
|
||||
onCreate={createFolder}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{reducedActions ? null : (
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={image.favorite ? 'Unfavorite' : 'Favorite'}>
|
||||
<ActionIcon
|
||||
color={image.favorite ? 'yellow' : 'gray'}
|
||||
variant='filled'
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(image.url, '_blank')}>
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Copy URL'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Download'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/r/${encodeURI(image.name)}?download=true`, '_blank')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Type
|
||||
file={image}
|
||||
sx={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
style={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
onClick={() => setOpen(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import { FileMeta } from '.';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
FileIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from '../icons';
|
||||
import Type from '../Type';
|
||||
|
||||
export default function FileModal({
|
||||
open,
|
||||
setOpen,
|
||||
file,
|
||||
loading,
|
||||
refresh,
|
||||
reducedActions = false,
|
||||
exifEnabled,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
file: any;
|
||||
loading: boolean;
|
||||
refresh: () => void;
|
||||
reducedActions?: boolean;
|
||||
exifEnabled?: boolean;
|
||||
}) {
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const folders = useFolders();
|
||||
|
||||
const [overrideRender, setOverrideRender] = useState(false);
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const handleDelete = async () => {
|
||||
deleteFile.mutate(file.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
setOpen(false);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
favoriteFile.mutate(
|
||||
{ id: file.id, favorite: !file.favorite },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const inFolder = file.folderId;
|
||||
|
||||
const removeFromFolder = async () => {
|
||||
const res = await useFetch('/api/user/folders/' + file.folderId, 'DELETE', {
|
||||
file: Number(file.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Removed from folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderMinusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to remove from folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addToFolder = async (t) => {
|
||||
const res = await useFetch('/api/user/folders/' + t, 'POST', {
|
||||
file: Number(file.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to add to folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (t) => {
|
||||
useFetch('/api/user/folders', 'POST', {
|
||||
name: t,
|
||||
add: [Number(file.id)],
|
||||
}).then((res) => {
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Created & added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { value: t, label: t };
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{file.name}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={file}
|
||||
src={`/r/${encodeURI(file.name)}`}
|
||||
alt={file.name}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
disableMediaPreview={false}
|
||||
overrideRender={overrideRender}
|
||||
setOverrideRender={setOverrideRender}
|
||||
/>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
cols={3}
|
||||
breakpoints={[
|
||||
{ maxWidth: 600, cols: 1 },
|
||||
{ maxWidth: 900, cols: 2 },
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={file.name} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={file.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={file?.views?.toLocaleString()} />
|
||||
{file.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={file?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(file.createdAt))}
|
||||
tooltip={new Date(file?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{file.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(file.expiresAt))}
|
||||
tooltip={new Date(file.expiresAt).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={file.id} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Group position='apart' my='md'>
|
||||
<Group position='left'>
|
||||
{exifEnabled && !reducedActions && (
|
||||
<Tooltip label='View Metadata'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
|
||||
>
|
||||
<InfoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
|
||||
>
|
||||
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
|
||||
<FolderMinusIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label='Add to folder'>
|
||||
<Select
|
||||
onChange={addToFolder}
|
||||
placeholder='Add to folder'
|
||||
data={[
|
||||
...(folders.data ? folders.data : []).map((folder) => ({
|
||||
value: String(folder.id),
|
||||
label: `${folder.id}: ${folder.name}`,
|
||||
})),
|
||||
]}
|
||||
searchable
|
||||
creatable
|
||||
getCreateLabel={(query) => `Create folder "${query}"`}
|
||||
onCreate={createFolder}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{reducedActions ? null : (
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={file.favorite ? 'Unfavorite' : 'Favorite'}>
|
||||
<ActionIcon
|
||||
color={file.favorite ? 'yellow' : 'gray'}
|
||||
variant='filled'
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Copy URL'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Download'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||