mirror of
https://github.com/diced/zipline.git
synced 2025-12-22 07:10:39 -08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
511f17e1a5 | ||
|
|
5b88b59724 | ||
|
|
1816e13879 | ||
|
|
1a837c02d2 | ||
|
|
f3634eff48 | ||
|
|
23ef407dd3 | ||
|
|
f40803f515 | ||
|
|
6b97d30a69 | ||
|
|
bd8d4e33fd | ||
|
|
70d48dd8c3 | ||
|
|
2e0a5f1d9c | ||
|
|
0ab814fc11 | ||
|
|
265760fb9c | ||
|
|
76ff3817af | ||
|
|
0dfe3fdcd1 | ||
|
|
5a522e0375 | ||
|
|
b15390f26c | ||
|
|
6fef197620 | ||
|
|
1d0bb2fa4f | ||
|
|
abb5bb5f25 | ||
|
|
4061da8622 | ||
|
|
6ef3c8274b | ||
|
|
e5ac971c8f | ||
|
|
b4ec1088d1 | ||
|
|
fe50bebeba | ||
|
|
1f61c56f83 | ||
|
|
cabf932ca0 | ||
|
|
f6b995c28d | ||
|
|
13a19ccd2b | ||
|
|
d1dea0cd92 | ||
|
|
b39507b9a8 | ||
|
|
633dfd4712 | ||
|
|
e6ed7a36d5 | ||
|
|
93cb9eec4c | ||
|
|
4849cd8221 | ||
|
|
89c58044a3 | ||
|
|
40fb11256f | ||
|
|
d112c3a509 | ||
|
|
23af36563f | ||
|
|
28db15eb77 | ||
|
|
e9054bd3e5 | ||
|
|
713f857e28 | ||
|
|
5d6768029f | ||
|
|
72e24a8b86 | ||
|
|
86c3e780d1 | ||
|
|
5102620953 | ||
|
|
4d728f9f8b | ||
|
|
faf5098357 | ||
|
|
c4066fc851 | ||
|
|
22633b8601 | ||
|
|
b873f99d46 | ||
|
|
a60d9c58b8 | ||
|
|
a2562c5ea2 | ||
|
|
1c674d3d9f | ||
|
|
fb32e9f38e | ||
|
|
6babf73e07 | ||
|
|
d0eb442fdf | ||
|
|
d3cb9118ce | ||
|
|
7ec6d566b8 | ||
|
|
d695211030 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,5 +7,5 @@ contact_links:
|
||||
url: https://discord.gg/EAhCRfGxCF
|
||||
about: Ask for help with anything related to Zipline!
|
||||
- name: Zipline Docs
|
||||
url: https://zipline.diced.tech
|
||||
url: https://zipline.diced.sh
|
||||
about: Maybe take a look a the docs?
|
||||
|
||||
0
.yarn/releases/yarn-3.3.1.cjs
vendored
Executable file → Normal file
0
.yarn/releases/yarn-3.3.1.cjs
vendored
Executable file → Normal file
48
Dockerfile
48
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Use the Prisma binaries image as the first stage
|
||||
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
|
||||
FROM ghcr.io/diced/prisma-binaries:5.1.x as prisma
|
||||
|
||||
# Use Alpine Linux as the second stage
|
||||
FROM node:18-alpine3.16 as base
|
||||
@@ -9,14 +9,6 @@ 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 ./
|
||||
@@ -26,51 +18,59 @@ 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_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
|
||||
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
|
||||
|
||||
FROM base as builder
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
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_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
|
||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
||||
PRISMA_CLIENT_ENGINE_TYPE=binary \
|
||||
ZIPLINE_DOCKER_BUILD=true \
|
||||
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 --from=builder /zipline/mimes.json ./mimes.json
|
||||
COPY --from=builder /zipline/next.config.js ./next.config.js
|
||||
COPY --from=builder /zipline/public ./public
|
||||
|
||||
# Copy Startup Script
|
||||
COPY docker-entrypoint.sh /zipline
|
||||
|
||||
# Make Startup Script Executable
|
||||
RUN chmod a+x /zipline/docker-entrypoint.sh && rm -rf /zipline/src
|
||||
|
||||
# Clean up
|
||||
RUN rm -rf /tmp/* /root/*
|
||||
RUN yarn cache clean --all
|
||||
|
||||
# Set the entrypoint to the startup script
|
||||
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 dicedtomato
|
||||
Copyright (c) 2024 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
|
||||
|
||||
32
README.md
32
README.md
@@ -35,17 +35,9 @@ A ShareX/file upload server that is easy to use, packed with features, and with
|
||||
- User invites
|
||||
- File Chunking (for large files)
|
||||
- File deletion once it reaches a certain amount of views
|
||||
- Automatic video thumbnail generation
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker compose up -d`)
|
||||
|
||||
<details>
|
||||
<summary>View upstream documentation</summary>
|
||||
|
||||
The website below provides documentation for more up-to-date features with the upstream branch. The normal documentation is for the latest release and is not updated unless a new release is made.
|
||||
|
||||
[https://trunk.zipline.diced.tech/](https://trunk.zipline.diced.tech/)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>Screenshots (click)</h2></summary>
|
||||
|
||||
@@ -76,17 +68,18 @@ Ways you could generate the string could be from a password managers generator,
|
||||
|
||||
## Building & running from source
|
||||
|
||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/).
|
||||
|
||||
It is recommended to not use npm, as it can cause issues with the build process.
|
||||
|
||||
Before you run `yarn build`, you might want to configure Zipline, as when building from source Zipline will need to read some sort of configuration. The only two variables needed are `CORE_SECRET` and `CORE_DATABASE_URL`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
# npm install
|
||||
yarn install
|
||||
# npm run build
|
||||
yarn build
|
||||
# npm start
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -119,7 +112,7 @@ 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)
|
||||
# Flameshot (Linux(Xorg/Wayland) and macOS)
|
||||
|
||||
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
|
||||
|
||||
@@ -134,6 +127,13 @@ After this, replace the `xsel -ib` with `wl-copy` in the script.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Mac instructions</summary>
|
||||
|
||||
If using macOS, you can replace the `xsel -ib` with `pbcopy` 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.
|
||||
|
||||
@@ -166,3 +166,7 @@ Create a discussion on GitHub, please include the following:
|
||||
## 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.
|
||||
|
||||
# Documentation
|
||||
|
||||
Documentation source code is located in [diced/zipline-docs](https://github.com/diced/zipline-docs), and can be accessed [here](https://zipl.vercel.app).
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
unset ZIPLINE_DOCKER_BUILD
|
||||
|
||||
node --enable-source-maps dist/index.js
|
||||
@@ -42,6 +42,9 @@
|
||||
["afm", ["application/octet-stream"]],
|
||||
["afp", ["application/vnd.ibm.modcap"]],
|
||||
["ahead", ["application/vnd.ahead.space"]],
|
||||
["ahk", ["text/autohotkey"]],
|
||||
["ahk1", ["text/autohotkey"]],
|
||||
["ahk2", ["text/autohotkey"]],
|
||||
["ai", ["application/postscript"]],
|
||||
["aif", ["audio/aiff"]],
|
||||
["aifc", ["audio/aiff"]],
|
||||
|
||||
108
package.json
108
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"version": "3.7.1",
|
||||
"version": "3.7.9",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
@@ -28,73 +28,73 @@
|
||||
"scripts:clear-temp": "node --enable-source-maps dist/scripts/clear-temp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^6.0.4",
|
||||
"@mantine/dropzone": "^6.0.4",
|
||||
"@mantine/form": "^6.0.4",
|
||||
"@mantine/hooks": "^6.0.4",
|
||||
"@mantine/modals": "^6.0.4",
|
||||
"@mantine/next": "^6.0.4",
|
||||
"@mantine/notifications": "^6.0.4",
|
||||
"@mantine/prism": "^6.0.4",
|
||||
"@mantine/spotlight": "^6.0.4",
|
||||
"@prisma/client": "^4.10.1",
|
||||
"@prisma/internals": "^4.10.1",
|
||||
"@prisma/migrate": "^4.10.1",
|
||||
"@sapphire/shapeshift": "^3.8.1",
|
||||
"@tabler/icons-react": "^2.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@mantine/core": "^6.0.21",
|
||||
"@mantine/dropzone": "^6.0.21",
|
||||
"@mantine/form": "^6.0.21",
|
||||
"@mantine/hooks": "^6.0.21",
|
||||
"@mantine/modals": "^6.0.21",
|
||||
"@mantine/next": "^6.0.21",
|
||||
"@mantine/notifications": "^6.0.21",
|
||||
"@mantine/prism": "^6.0.21",
|
||||
"@mantine/spotlight": "^6.0.21",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@prisma/internals": "^5.1.1",
|
||||
"@prisma/migrate": "^5.1.1",
|
||||
"@sapphire/shapeshift": "^3.9.3",
|
||||
"@tabler/icons-react": "^2.41.0",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"argon2": "^0.30.3",
|
||||
"cookie": "^0.5.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"argon2": "^0.31.2",
|
||||
"cookie": "^0.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^16.3.1",
|
||||
"dotenv-expand": "^10.0.0",
|
||||
"exiftool-vendored": "^21.2.0",
|
||||
"fastify": "^4.15.0",
|
||||
"fastify-plugin": "^4.5.0",
|
||||
"fflate": "^0.7.4",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"find-my-way": "^7.6.0",
|
||||
"katex": "^0.16.4",
|
||||
"mantine-datatable": "^2.2.6",
|
||||
"minio": "^7.0.33",
|
||||
"exiftool-vendored": "^23.4.0",
|
||||
"fastify": "^4.24.3",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"fflate": "^0.8.1",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"find-my-way": "^7.7.0",
|
||||
"katex": "^0.16.9",
|
||||
"mantine-datatable": "^2.9.14",
|
||||
"minio": "^7.1.3",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^13.2.4",
|
||||
"next": "^14.0.3",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^4.10.1",
|
||||
"prisma": "^5.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"recharts": "^2.5.0",
|
||||
"recharts": "^2.10.1",
|
||||
"recoil": "^0.7.7",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.32.0"
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.32.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/minio": "^7.0.17",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.15.10",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "^18.0.29",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"@types/cookie": "^0.5.4",
|
||||
"@types/katex": "^0.16.6",
|
||||
"@types/minio": "^7.1.1",
|
||||
"@types/multer": "^1.4.10",
|
||||
"@types/node": "^18.18.10",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-next": "^13.2.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.7",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.0.2"
|
||||
"prettier": "^3.1.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "File" ALTER COLUMN "size" SET DATA TYPE BIGINT;
|
||||
@@ -48,7 +48,7 @@ model File {
|
||||
originalName String?
|
||||
mimetype String @default("image/png")
|
||||
createdAt DateTime @default(now())
|
||||
size Int @default(0)
|
||||
size BigInt @default(0)
|
||||
expiresAt DateTime?
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
@@ -63,7 +63,7 @@ model File {
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId Int?
|
||||
|
||||
thumbnail Thumbnail?
|
||||
thumbnail Thumbnail?
|
||||
}
|
||||
|
||||
model Thumbnail {
|
||||
|
||||
@@ -1498,4 +1498,4 @@ wheat
|
||||
white
|
||||
whitesmoke
|
||||
yellow
|
||||
yellowgreen
|
||||
yellowgreen
|
||||
@@ -1747,4 +1747,4 @@ zigzagsalamander
|
||||
zonetailedpigeon
|
||||
zooplankton
|
||||
zopilote
|
||||
zorilla
|
||||
zorilla
|
||||
@@ -125,7 +125,7 @@ export default function FileModal({
|
||||
icon: <IconPhotoCancel size='1rem' />,
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ export default function File({
|
||||
},
|
||||
transition: 'filter 0.2s ease-in-out',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
shadow='md'
|
||||
onClick={() => setOpen(true)}
|
||||
|
||||
@@ -280,7 +280,7 @@ export default function Layout({ children, props }) {
|
||||
component={Link}
|
||||
href={link}
|
||||
/>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Navbar.Section>
|
||||
<Navbar.Section>
|
||||
@@ -360,6 +360,11 @@ export default function Layout({ children, props }) {
|
||||
compact
|
||||
size='xl'
|
||||
p='sm'
|
||||
styles={{
|
||||
label: {
|
||||
overflow: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{user.username}
|
||||
</Button>
|
||||
@@ -411,16 +416,20 @@ export default function Layout({ children, props }) {
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<>
|
||||
{oauth_providers.filter((x) =>
|
||||
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
|
||||
{oauth_providers.filter(
|
||||
(x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase()),
|
||||
).length ? (
|
||||
<Menu.Label>Connected Accounts</Menu.Label>
|
||||
) : null}
|
||||
{oauth_providers
|
||||
.filter((x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase())
|
||||
.filter(
|
||||
(x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase()),
|
||||
)
|
||||
.map(({ name, Icon }, i) => (
|
||||
<>
|
||||
@@ -433,8 +442,11 @@ export default function Layout({ children, props }) {
|
||||
</Menu.Item>
|
||||
</>
|
||||
))}
|
||||
{oauth_providers.filter((x) =>
|
||||
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
|
||||
{oauth_providers.filter(
|
||||
(x) =>
|
||||
user.oauth
|
||||
?.map(({ provider }) => provider.toLowerCase())
|
||||
.includes(x.name.toLowerCase()),
|
||||
).length ? (
|
||||
<Menu.Divider />
|
||||
) : null}
|
||||
|
||||
@@ -27,7 +27,7 @@ import PrismCode from './render/PrismCode';
|
||||
|
||||
function PlaceholderContent({ text, Icon }) {
|
||||
return (
|
||||
<Group sx={(t) => ({ color: t.colors.dark[2] })}>
|
||||
<Group sx={(t) => ({ color: t.colors.dark[2], padding: 3, justifyContent: 'center' })}>
|
||||
<Icon size={48} />
|
||||
<Text size='md'>{text}</Text>
|
||||
</Group>
|
||||
@@ -60,7 +60,7 @@ function VideoThumbnailPlaceholder({ file, mediaPreview, ...props }) {
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Image
|
||||
src={file.thumbnail}
|
||||
src={typeof file.thumbnail === 'string' ? file.thumbnail : `/r/${file.thumbnail.name}`}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Dropzone({ loading, onDrop, children }) {
|
||||
]}
|
||||
>
|
||||
<MantineDropzone loading={loading} onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 440, flexDirection: 'column' }}>
|
||||
<IconPhoto size={80} />
|
||||
|
||||
<Text size='xl' inline>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
{ shallow: true },
|
||||
);
|
||||
|
||||
const { count } = await useFetch(`/api/user/paged?count=true${!checked ? '&filter=media' : ''}`);
|
||||
|
||||
@@ -50,7 +50,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
|
||||
if (values.count < 1 || values.count > 100)
|
||||
return form.setFieldError('count', 'Must be between 1 and 100');
|
||||
const expiresAt = values.expires === 'never' ? null : expireReadToDate(values.expires);
|
||||
const expiresAt = expireReadToDate(values.expires);
|
||||
|
||||
setOpen(false);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Flameshot({ user, open, setOpen }) {
|
||||
let shell;
|
||||
if (values.type === 'upload-file') {
|
||||
shell = `#!/bin/bash${values.wlCompositorNotSupported ? '\nexport XDG_CURRENT_DESKTOP=sway\n' : ''}
|
||||
flameshot gui -r > /tmp/ss.png;
|
||||
flameshot gui -r > /tmp/ss.png;if [ ! -s /tmp/ss.png ]; then\n exit 1\nfi
|
||||
${curl.join(' ')}${values.noJSON ? '' : " | jq -r '.files[0]'"} | tr -d '\\n' | ${
|
||||
values.wlCompatibility ? 'wl-copy' : 'xsel -ib'
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function ShareX({ user, open, setOpen }) {
|
||||
const pseudoElement = document.createElement('a');
|
||||
pseudoElement.setAttribute(
|
||||
'href',
|
||||
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))
|
||||
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t')),
|
||||
);
|
||||
pseudoElement.setAttribute('download', `zipline${values.type === 'upload-file' ? '' : '-url'}.sxcu`);
|
||||
pseudoElement.style.display = 'none';
|
||||
|
||||
@@ -268,7 +268,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
size: s.size,
|
||||
full: s.name,
|
||||
}))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime()),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -367,8 +367,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Title>Manage User</Title>
|
||||
<MutedText size='md'>
|
||||
Want to use variables in embed text? Visit{' '}
|
||||
<AnchorNext href='https://zipline.diced.tech/docs/guides/variables'>the docs</AnchorNext> for
|
||||
variables
|
||||
<AnchorNext href='https://zipline.diced.sh/docs/guides/variables'>the docs</AnchorNext> for variables
|
||||
</MutedText>
|
||||
|
||||
<TextInput
|
||||
@@ -488,7 +487,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
{oauth_providers
|
||||
.filter(
|
||||
(x) =>
|
||||
!user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
|
||||
!user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase()),
|
||||
)
|
||||
.map(({ link_url, name, Icon }, i) => (
|
||||
<Button key={i} size='lg' leftIcon={<Icon />} component={Link} href={link_url} my='sm'>
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
return e.returnValue;
|
||||
}
|
||||
},
|
||||
[loading]
|
||||
[loading],
|
||||
);
|
||||
|
||||
const beforeRouteChange = useCallback(
|
||||
@@ -56,7 +56,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[loading]
|
||||
[loading],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,6 +85,14 @@ export default function File({ chunks: chunks_config }) {
|
||||
}, [loading, beforeUnload, beforeRouteChange]);
|
||||
|
||||
const handleChunkedFiles = async (expiresAt: Date, toChunkFiles: File[]) => {
|
||||
if (!chunks_config.enabled)
|
||||
return showNotification({
|
||||
id: 'upload-chunked',
|
||||
title: 'Chunked files are disabled',
|
||||
message: 'This should not be called, but some how got called...',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
for (let i = 0; i !== toChunkFiles.length; ++i) {
|
||||
const file = toChunkFiles[i];
|
||||
const identifier = randomChars(4);
|
||||
@@ -183,7 +191,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
ready = false;
|
||||
}
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
req.open('POST', '/api/upload');
|
||||
@@ -222,10 +230,10 @@ export default function File({ chunks: chunks_config }) {
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
if (file.size >= chunks_config.max_size) {
|
||||
if (chunks_config.enabled && file.size >= chunks_config.max_size) {
|
||||
toChunkFiles.push(file);
|
||||
} else {
|
||||
body.append('file', files[i]);
|
||||
body.append('file', files[i], encodeURIComponent(files[i].name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +307,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
}
|
||||
setProgress(0);
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
if (bodyLength !== 0) {
|
||||
|
||||
@@ -213,7 +213,7 @@ export function OptionsModal({
|
||||
export default function useUploadOptions(): [
|
||||
UploadOptionsState,
|
||||
Dispatch<SetStateAction<boolean>>,
|
||||
ReactNode
|
||||
ReactNode,
|
||||
] {
|
||||
const [state, setState] = useReducer((state, newState) => ({ ...state, ...newState }), {
|
||||
expires: 'never',
|
||||
|
||||
@@ -26,7 +26,7 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
};
|
||||
|
||||
setOpen(false);
|
||||
const res = await useFetch('/api/auth/create', 'POST', data);
|
||||
const res = await useFetch('/api/auth/register', 'POST', data);
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Failed to create user',
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface ConfigUploader {
|
||||
format_date: string;
|
||||
default_expiration: string;
|
||||
assume_mimetypes: boolean;
|
||||
random_words_separator: string;
|
||||
}
|
||||
|
||||
export interface ConfigUrls {
|
||||
@@ -135,9 +136,12 @@ export interface ConfigOAuth {
|
||||
|
||||
discord_client_id?: string;
|
||||
discord_client_secret?: string;
|
||||
discord_redirect_uri?: string;
|
||||
discord_whitelisted_users?: string[];
|
||||
|
||||
google_client_id?: string;
|
||||
google_client_secret?: string;
|
||||
google_redirect_uri?: string;
|
||||
}
|
||||
|
||||
export interface ConfigChunks {
|
||||
|
||||
@@ -98,6 +98,7 @@ export default function readConfig() {
|
||||
map('UPLOADER_FORMAT_DATE', 'string', 'uploader.format_date'),
|
||||
map('UPLOADER_DEFAULT_EXPIRATION', 'string', 'uploader.default_expiration'),
|
||||
map('UPLOADER_ASSUME_MIMETYPES', 'boolean', 'uploader.assume_mimetypes'),
|
||||
map('UPLOADER_RANDOM_WORDS_SEPARATOR', 'string', 'uploader.random_words_separator'),
|
||||
|
||||
map('URLS_ROUTE', 'string', 'urls.route'),
|
||||
map('URLS_LENGTH', 'number', 'urls.length'),
|
||||
@@ -146,9 +147,12 @@ export default function readConfig() {
|
||||
|
||||
map('OAUTH_DISCORD_CLIENT_ID', 'string', 'oauth.discord_client_id'),
|
||||
map('OAUTH_DISCORD_CLIENT_SECRET', 'string', 'oauth.discord_client_secret'),
|
||||
map('OAUTH_DISCORD_REDIRECT_URI', 'string', 'oauth.discord_redirect_uri'),
|
||||
map('OAUTH_DISCORD_WHITELISTED_USERS', 'array', 'oauth.discord_whitelisted_users'),
|
||||
|
||||
map('OAUTH_GOOGLE_CLIENT_ID', 'string', 'oauth.google_client_id'),
|
||||
map('OAUTH_GOOGLE_CLIENT_SECRET', 'string', 'oauth.google_client_secret'),
|
||||
map('OAUTH_GOOGLE_REDIRECT_URI', 'string', 'oauth.google_redirect_uri'),
|
||||
|
||||
map('FEATURES_INVITES', 'boolean', 'features.invites'),
|
||||
map('FEATURES_INVITES_LENGTH', 'number', 'features.invites_length'),
|
||||
|
||||
@@ -97,6 +97,7 @@ const validator = s.object({
|
||||
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
|
||||
default_expiration: s.string.optional.default(null),
|
||||
assume_mimetypes: s.boolean.default(false),
|
||||
random_words_separator: s.string.default('-'),
|
||||
})
|
||||
.default({
|
||||
default_format: 'RANDOM',
|
||||
@@ -140,11 +141,11 @@ const validator = s.object({
|
||||
s.object({
|
||||
label: s.string,
|
||||
link: s.string,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.default([
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.sh/' },
|
||||
]),
|
||||
})
|
||||
.default({
|
||||
@@ -155,7 +156,7 @@ const validator = s.object({
|
||||
|
||||
external_links: [
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.sh/' },
|
||||
],
|
||||
}),
|
||||
discord: s
|
||||
@@ -176,9 +177,12 @@ const validator = s.object({
|
||||
|
||||
discord_client_id: s.string.nullable.default(null),
|
||||
discord_client_secret: s.string.nullable.default(null),
|
||||
discord_redirect_uri: s.string.nullable.default(null),
|
||||
discord_whitelisted_users: s.string.array.default([]),
|
||||
|
||||
google_client_id: s.string.nullable.default(null),
|
||||
google_client_secret: s.string.nullable.default(null),
|
||||
google_redirect_uri: s.string.nullable.default(null),
|
||||
})
|
||||
.nullish.default(null),
|
||||
features: s
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Readable } from 'stream';
|
||||
export abstract class Datasource {
|
||||
public name: string;
|
||||
|
||||
public abstract save(file: string, data: Buffer): Promise<void>;
|
||||
public abstract save(file: string, data: Buffer, options?: { type: string }): Promise<void>;
|
||||
public abstract delete(file: string): Promise<void>;
|
||||
public abstract clear(): Promise<void>;
|
||||
public abstract size(file: string): Promise<number>;
|
||||
|
||||
@@ -20,8 +20,13 @@ export class S3 extends Datasource {
|
||||
});
|
||||
}
|
||||
|
||||
public async save(file: string, data: Buffer): Promise<void> {
|
||||
await this.s3.putObject(this.config.bucket, file, data);
|
||||
public async save(file: string, data: Buffer, options?: { type: string }): Promise<void> {
|
||||
await this.s3.putObject(
|
||||
this.config.bucket,
|
||||
file,
|
||||
data,
|
||||
options ? { 'Content-Type': options.type } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
public async delete(file: string): Promise<void> {
|
||||
@@ -49,13 +54,10 @@ export class S3 extends Datasource {
|
||||
});
|
||||
}
|
||||
|
||||
public size(file: string): Promise<number> {
|
||||
return new Promise((res) => {
|
||||
this.s3.statObject(this.config.bucket, file, (err, stat) => {
|
||||
if (err) res(0);
|
||||
else res(stat.size);
|
||||
});
|
||||
});
|
||||
public async size(file: string): Promise<number> {
|
||||
const stat = await this.s3.statObject(this.config.bucket, file);
|
||||
|
||||
return stat.size;
|
||||
}
|
||||
|
||||
public async fullSize(): Promise<number> {
|
||||
|
||||
@@ -8,7 +8,7 @@ const logger = Logger.get('discord');
|
||||
|
||||
export function parseContent(
|
||||
content: ConfigDiscordContent,
|
||||
args: ParseValue
|
||||
args: ParseValue,
|
||||
): ConfigDiscordContent & { url: string } {
|
||||
return {
|
||||
content: content.content ? parseString(content.content, args) : null,
|
||||
@@ -28,10 +28,10 @@ export function parseContent(
|
||||
}
|
||||
|
||||
export async function sendUpload(user: User, file: File, raw_link: string, link: string) {
|
||||
if (!config.discord.upload) return;
|
||||
if (!config.discord.url && !config.discord.upload.url) return;
|
||||
if (!config.discord.upload) return logger.debug('no discord upload config, no webhook sent');
|
||||
if (!config.discord.url && !config.discord.upload.url)
|
||||
return logger.debug('no discord url, no webhook sent');
|
||||
|
||||
logger.debug(`discord config:\n${JSON.stringify(config.discord)}`);
|
||||
const parsed = parseContent(config.discord.upload, {
|
||||
file,
|
||||
user,
|
||||
@@ -97,8 +97,9 @@ export async function sendUpload(user: User, file: File, raw_link: string, link:
|
||||
}
|
||||
|
||||
export async function sendShorten(user: User, url: Url, link: string) {
|
||||
if (!config.discord.shorten) return;
|
||||
if (!config.discord.url && !config.discord.shorten.url) return;
|
||||
if (!config.discord.shorten) return logger.debug('no discord shorten config, no webhook sent');
|
||||
if (!config.discord.url && !config.discord.shorten.url)
|
||||
return logger.debug('no discord url, no webhook sent');
|
||||
|
||||
const parsed = parseContent(config.discord.shorten, {
|
||||
url,
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
const logger = Logger.get('random_words');
|
||||
|
||||
export type GfyCatWords = {
|
||||
adjectives: string[];
|
||||
animals: string[];
|
||||
};
|
||||
|
||||
export async function importWords(): Promise<GfyCatWords> {
|
||||
const adjectives = (await readFile('public/adjectives.txt', 'utf-8')).split('\n');
|
||||
const animals = (await readFile('public/animals.txt', 'utf-8')).split('\n');
|
||||
export async function importWords(): Promise<GfyCatWords | null> {
|
||||
try {
|
||||
const adjectives = (await readFile('public/adjectives.txt', 'utf-8')).split('\n').map((x) => x.trim());
|
||||
const animals = (await readFile('public/animals.txt', 'utf-8')).split('\n').map((x) => x.trim());
|
||||
|
||||
return {
|
||||
adjectives,
|
||||
animals,
|
||||
};
|
||||
return {
|
||||
adjectives,
|
||||
animals,
|
||||
};
|
||||
} catch {
|
||||
logger.error('public/adjectives.txt or public/animals.txt do not exist, to fix this please retrieve.');
|
||||
logger.error('to prevent this from happening again, remember to not delete your public/ directory.');
|
||||
logger.error('file names will use the RANDOM format instead until fixed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function randomWord(words: string[]) {
|
||||
return words[Math.floor(Math.random() * words.length)];
|
||||
}
|
||||
|
||||
export default async function gfycat() {
|
||||
export default async function gfycat(): Promise<string | null> {
|
||||
const words = await importWords();
|
||||
|
||||
return `${randomWord(words.adjectives)}${randomWord(words.adjectives)}${randomWord(words.animals)}`;
|
||||
if (!words) return null;
|
||||
|
||||
return `${randomWord(words.adjectives)}${config.uploader.random_words_separator}${randomWord(
|
||||
words.adjectives,
|
||||
)}${config.uploader.random_words_separator}${randomWord(words.animals)}`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async function formatFileName(nameFormat: NameFormat, originalNam
|
||||
|
||||
return name;
|
||||
case 'gfycat':
|
||||
return gfycat();
|
||||
return gfycat() ?? random();
|
||||
default:
|
||||
return random();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export type ApiError = {
|
||||
export default async function useFetch(
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
|
||||
body: ApiError | Record<string, unknown> = null
|
||||
body: ApiError | Record<string, unknown> = null,
|
||||
) {
|
||||
const headers = {};
|
||||
if (body) headers['content-type'] = 'application/json';
|
||||
|
||||
@@ -60,8 +60,8 @@ export default class Logger {
|
||||
this.formatMessage(
|
||||
LoggerLevel.ERROR,
|
||||
this.name,
|
||||
args.map((error) => (typeof error === 'string' ? error : (error as Error).stack)).join(' ')
|
||||
)
|
||||
args.map((error) => (typeof error === 'string' ? error : (error as Error).stack)).join(' '),
|
||||
),
|
||||
);
|
||||
|
||||
return this;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import config from 'lib/config';
|
||||
import { notNull } from 'lib/util';
|
||||
import { isNotNullOrUndefined } from 'lib/util';
|
||||
import { GetServerSideProps } from 'next';
|
||||
|
||||
export type OauthProvider = {
|
||||
@@ -19,6 +19,7 @@ export type ServerSideProps = {
|
||||
bypass_local_login: boolean;
|
||||
chunks_size: number;
|
||||
max_size: number;
|
||||
chunks_enabled: boolean;
|
||||
totp_enabled: boolean;
|
||||
exif_enabled: boolean;
|
||||
fileId?: string;
|
||||
@@ -26,9 +27,15 @@ export type ServerSideProps = {
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ctx) => {
|
||||
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
|
||||
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
|
||||
const googleEnabled = notNull(config.oauth?.google_client_id, config.oauth?.google_client_secret);
|
||||
const ghEnabled =
|
||||
isNotNullOrUndefined(config.oauth?.github_client_id) &&
|
||||
isNotNullOrUndefined(config.oauth?.github_client_secret);
|
||||
const discEnabled =
|
||||
isNotNullOrUndefined(config.oauth?.discord_client_id) &&
|
||||
isNotNullOrUndefined(config.oauth?.discord_client_secret);
|
||||
const googleEnabled =
|
||||
isNotNullOrUndefined(config.oauth?.google_client_id) &&
|
||||
isNotNullOrUndefined(config.oauth?.google_client_secret);
|
||||
|
||||
const oauth_providers: OauthProvider[] = [];
|
||||
|
||||
@@ -65,6 +72,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
|
||||
chunks_size: config.chunks.chunks_size,
|
||||
max_size: config.chunks.max_size,
|
||||
totp_enabled: config.mfa.totp_enabled,
|
||||
chunks_enabled: config.chunks.enabled,
|
||||
exif_enabled: config.exif.enabled,
|
||||
compress: config.core.compression.on_dashboard,
|
||||
} as ServerSideProps,
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface OAuthResponse {
|
||||
export const withOAuth =
|
||||
(
|
||||
provider: 'discord' | 'github' | 'google',
|
||||
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>
|
||||
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
||||
) =>
|
||||
async (req: NextApiReq, res: NextApiRes) => {
|
||||
const logger = Logger.get(`oauth::${provider}`);
|
||||
@@ -172,7 +172,7 @@ export const withOAuth =
|
||||
|
||||
res.setUserCookie(existingOauth.userId);
|
||||
Logger.get('user').info(
|
||||
`User ${existingOauth.username} (${existingOauth.id}) logged in via oauth(${provider})`
|
||||
`User ${existingOauth.username} (${existingOauth.id}) logged in via oauth(${provider})`,
|
||||
);
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
|
||||
@@ -66,7 +66,7 @@ export type ZiplineApiConfig = {
|
||||
export const withZipline =
|
||||
(
|
||||
handler: (req: NextApiRequest, res: NextApiResponse, user?: UserExtended) => Promise<unknown>,
|
||||
api_config: ZiplineApiConfig = { methods: ['GET', 'OPTIONS'] }
|
||||
api_config: ZiplineApiConfig = { methods: ['GET', 'OPTIONS'] },
|
||||
) =>
|
||||
(req: NextApiReq, res: NextApiRes) => {
|
||||
if (!api_config.methods.includes('OPTIONS')) api_config.methods.push('OPTIONS');
|
||||
@@ -87,7 +87,7 @@ export const withZipline =
|
||||
code: 400,
|
||||
...extra,
|
||||
},
|
||||
400
|
||||
400,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,7 +99,7 @@ export const withZipline =
|
||||
code: 401,
|
||||
...extra,
|
||||
},
|
||||
401
|
||||
401,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ export const withZipline =
|
||||
code: 403,
|
||||
...extra,
|
||||
},
|
||||
403
|
||||
403,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export const withZipline =
|
||||
code: 404,
|
||||
...extra,
|
||||
},
|
||||
404
|
||||
404,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,7 +136,7 @@ export const withZipline =
|
||||
code: 429,
|
||||
...extra,
|
||||
},
|
||||
429
|
||||
429,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,7 +161,7 @@ export const withZipline =
|
||||
path: '/',
|
||||
expires: new Date(1),
|
||||
maxAge: undefined,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -230,7 +230,7 @@ export const withZipline =
|
||||
error: 'method not allowed',
|
||||
code: 405,
|
||||
},
|
||||
405
|
||||
405,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ export const github_auth = {
|
||||
};
|
||||
|
||||
export const discord_auth = {
|
||||
oauth_url: (clientId: string, origin: string, state?: string) =>
|
||||
oauth_url: (clientId: string, origin: string, state?: string, redirect_uri?: string) =>
|
||||
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
`${origin}/api/auth/oauth/discord`
|
||||
redirect_uri || `${origin}/api/auth/oauth/discord`,
|
||||
)}&response_type=code&scope=identify${state ? `&state=${state}` : ''}`,
|
||||
oauth_user: async (access_token: string) => {
|
||||
const res = await fetch('https://discord.com/api/users/@me', {
|
||||
@@ -33,15 +33,15 @@ export const discord_auth = {
|
||||
};
|
||||
|
||||
export const google_auth = {
|
||||
oauth_url: (clientId: string, origin: string, state?: string) =>
|
||||
oauth_url: (clientId: string, origin: string, state?: string, redirect_uri?: string) =>
|
||||
`https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
`${origin}/api/auth/oauth/google`
|
||||
redirect_uri || `${origin}/api/auth/oauth/google`,
|
||||
)}&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.profile${
|
||||
state ? `&state=${state}` : ''
|
||||
}`,
|
||||
oauth_user: async (access_token: string) => {
|
||||
const res = await fetch(
|
||||
`https://people.googleapis.com/v1/people/me?access_token=${access_token}&personFields=names,photos`
|
||||
`https://people.googleapis.com/v1/people/me?access_token=${access_token}&personFields=names,photos`,
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const useFiles = (query: { [key: string]: string } = {}) => {
|
||||
...x,
|
||||
createdAt: new Date(x.createdAt),
|
||||
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -59,7 +59,7 @@ export const usePaginatedFiles = (page?: number, options?: Partial<PaginatedFile
|
||||
...x,
|
||||
createdAt: new Date(x.createdAt),
|
||||
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export const useRecent = (filter?: string) => {
|
||||
...x,
|
||||
createdAt: new Date(x.createdAt),
|
||||
expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -94,7 +94,7 @@ export function useFileDelete() {
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries(['files']);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export function useFileFavorite() {
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries(['files']);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,27 +17,17 @@ export const useFolders = (query: { [key: string]: string } = {}) => {
|
||||
const queryString = queryBuilder.toString();
|
||||
|
||||
return useQuery<UserFoldersResponse[]>(['folders', queryString], async () => {
|
||||
return fetch('/api/user/folders?' + queryString)
|
||||
.then((res) => res.json() as Promise<UserFoldersResponse[]>)
|
||||
.then((data) =>
|
||||
data.map((x) => ({
|
||||
...x,
|
||||
createdAt: new Date(x.createdAt).toLocaleString(),
|
||||
updatedAt: new Date(x.updatedAt).toLocaleString(),
|
||||
}))
|
||||
);
|
||||
return fetch('/api/user/folders?' + queryString).then(
|
||||
(res) => res.json() as Promise<UserFoldersResponse[]>,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useFolder = (id: string, withFiles = false) => {
|
||||
return useQuery<UserFoldersResponse>(['folder', id], async () => {
|
||||
return fetch('/api/user/folders/' + id + (withFiles ? '?files=true' : ''))
|
||||
.then((res) => res.json() as Promise<UserFoldersResponse>)
|
||||
.then((data) => ({
|
||||
...data,
|
||||
createdAt: new Date(data.createdAt).toLocaleString(),
|
||||
updatedAt: new Date(data.updatedAt).toLocaleString(),
|
||||
}));
|
||||
return fetch('/api/user/folders/' + id + (withFiles ? '?files=true' : '')).then(
|
||||
(res) => res.json() as Promise<UserFoldersResponse>,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@ export const useStats = (amount = 2) => {
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,6 @@ export function useURLDelete() {
|
||||
?.filter((u) => u.id !== variables);
|
||||
queryClient.setQueryData(['urls'], dataWithoutDeleted);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ export const useVersion = () => {
|
||||
return useQuery<VersionResponse>(
|
||||
['version'],
|
||||
async () => {
|
||||
return fetch('/api/version').then((res) => res.json());
|
||||
return fetch('/api/version').then((res) => (res.ok ? res.json() : Promise.reject('')));
|
||||
},
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
refetchInterval: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
|
||||
title: string,
|
||||
description: string,
|
||||
link: string,
|
||||
icon: ReactNode
|
||||
icon: ReactNode,
|
||||
): SpotlightAction => {
|
||||
return actionDo(group, title, description, icon, () => linkTo(link));
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
|
||||
title: string,
|
||||
description: string,
|
||||
icon: ReactNode,
|
||||
action: () => void
|
||||
action: () => void,
|
||||
): SpotlightAction => {
|
||||
return {
|
||||
group,
|
||||
@@ -70,7 +70,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
|
||||
'Manage Account',
|
||||
'Manage your account settings',
|
||||
'/dashboard/manage',
|
||||
<IconUser />
|
||||
<IconUser />,
|
||||
),
|
||||
|
||||
// Actions
|
||||
@@ -80,14 +80,14 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
|
||||
'Upload Files',
|
||||
'Upload files of any kind',
|
||||
'/dashboard/upload/file',
|
||||
<IconFileUpload />
|
||||
<IconFileUpload />,
|
||||
),
|
||||
actionLink(
|
||||
'Actions',
|
||||
'Upload Text',
|
||||
'Upload code, or any other kind of text file',
|
||||
'/dashboard/upload/text',
|
||||
<IconFileText />
|
||||
<IconFileText />,
|
||||
),
|
||||
actionDo('Actions', 'Copy Token', 'Copy your API token to your clipboard', <IconClipboardCopy />, () => {
|
||||
clipboard.copy(user.token);
|
||||
@@ -99,7 +99,7 @@ export const createSpotlightActions = (router: NextRouter): SpotlightAction[] =>
|
||||
});
|
||||
}),
|
||||
|
||||
actionLink('Help', 'Documentation', 'View the documentation', 'https://zipline.diced.tech', <IconHelp />),
|
||||
actionLink('Help', 'Documentation', 'View the documentation', 'https://zipline.diced.sh', <IconHelp />),
|
||||
|
||||
// the list of actions here is very incomplete, and will be expanded in the future
|
||||
];
|
||||
|
||||
@@ -120,6 +120,6 @@ export async function getBase64URLFromURL(url: string) {
|
||||
return `data:${res.headers.get('content-type')};base64,${base64}`;
|
||||
}
|
||||
|
||||
export function notNull(a: unknown, b: unknown) {
|
||||
return a !== null && b !== null;
|
||||
export function isNotNullOrUndefined(value: unknown) {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
@@ -24,16 +24,16 @@ export function humanToBytes(value: string): number {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function bytesToHuman(value: number): string {
|
||||
if (isNaN(value)) return '0.0 B';
|
||||
export function bytesToHuman(value: number | bigint): string {
|
||||
if (typeof value !== 'bigint' && isNaN(value)) return '0.0 B';
|
||||
if (value === Infinity) return '0.0 B';
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; // if people upload stuff bigger than a petabyte then idk
|
||||
let num = 0;
|
||||
|
||||
while (value > 1024) {
|
||||
value /= 1024;
|
||||
value = Number(value) / 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[num]}`;
|
||||
return `${Number(value).toFixed(1)} ${units[num] || ''}`;
|
||||
}
|
||||
|
||||
@@ -51,22 +51,22 @@ export function humanTime(string: StringValue | string): Date {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseExpiry(header: string): Date | null {
|
||||
if (!header) return null;
|
||||
export function parseExpiry(header: string): Date {
|
||||
if (!header) throw new Error('no expiry provided');
|
||||
header = header.toLowerCase();
|
||||
|
||||
if (header.startsWith('date=')) {
|
||||
const date = new Date(header.substring(5));
|
||||
|
||||
if (!date.getTime()) return null;
|
||||
if (date.getTime() < Date.now()) return null;
|
||||
if (!date.getTime()) throw new Error('invalid date');
|
||||
if (date.getTime() < Date.now()) throw new Error('expiry must be in the future');
|
||||
return date;
|
||||
}
|
||||
|
||||
const human = humanTime(header);
|
||||
|
||||
if (!human) return null;
|
||||
if (human.getTime() < Date.now()) return null;
|
||||
if (!human) throw new Error('failed to parse human time');
|
||||
if (human.getTime() < Date.now()) throw new Error('expiry must be in the future');
|
||||
|
||||
return human;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export function expireReadToDate(expires: string): Date {
|
||||
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
|
||||
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
|
||||
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
|
||||
}[expires]
|
||||
}[expires],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function removeGPSData(image: File): Promise<void> {
|
||||
|
||||
logger.debug(`reading file to upload to datasource: ${file} -> ${image.name}`);
|
||||
const buffer = await readFile(file);
|
||||
await datasource.save(image.name, buffer);
|
||||
await datasource.save(image.name, buffer, { type: image.mimetype });
|
||||
|
||||
logger.debug(`removing temp file: ${file}`);
|
||||
await rm(file);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { File, User, Url } from '@prisma/client';
|
||||
import { bytesToHuman } from './bytes';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
export type ParseValue = {
|
||||
file?: File;
|
||||
@@ -9,6 +11,8 @@ export type ParseValue = {
|
||||
raw_link?: string;
|
||||
};
|
||||
|
||||
const logger = Logger.get('parser');
|
||||
|
||||
export function parseString(str: string, value: ParseValue) {
|
||||
if (!str) return null;
|
||||
str = str
|
||||
@@ -16,7 +20,7 @@ export function parseString(str: string, value: ParseValue) {
|
||||
.replace(/\{raw_link\}/gi, value.raw_link)
|
||||
.replace(/\\n/g, '\n');
|
||||
|
||||
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?\}/gi;
|
||||
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?(::(?<mod_tzlocale>\S+))?\}/gi;
|
||||
let matches: RegExpMatchArray;
|
||||
|
||||
while ((matches = re.exec(str))) {
|
||||
@@ -32,12 +36,13 @@ export function parseString(str: string, value: ParseValue) {
|
||||
re.lastIndex = matches.index;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (['originalName', 'name'].includes(matches.groups.prop)) {
|
||||
str = replaceCharsFromString(
|
||||
str,
|
||||
decodeURIComponent(escape(getV[matches.groups.prop])),
|
||||
matches.index,
|
||||
re.lastIndex
|
||||
re.lastIndex,
|
||||
);
|
||||
re.lastIndex = matches.index;
|
||||
continue;
|
||||
@@ -52,7 +57,12 @@ export function parseString(str: string, value: ParseValue) {
|
||||
}
|
||||
|
||||
if (matches.groups.mod) {
|
||||
str = replaceCharsFromString(str, modifier(matches.groups.mod, v), matches.index, re.lastIndex);
|
||||
str = replaceCharsFromString(
|
||||
str,
|
||||
modifier(matches.groups.mod, v, matches.groups.mod_tzlocale ?? undefined),
|
||||
matches.index,
|
||||
re.lastIndex,
|
||||
);
|
||||
re.lastIndex = matches.index;
|
||||
continue;
|
||||
}
|
||||
@@ -64,17 +74,42 @@ export function parseString(str: string, value: ParseValue) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function modifier(mod: string, value: unknown): string {
|
||||
function modifier(mod: string, value: unknown, tzlocale?: string): string {
|
||||
mod = mod.toLowerCase();
|
||||
|
||||
if (value instanceof Date) {
|
||||
const args = [undefined, undefined];
|
||||
|
||||
if (tzlocale) {
|
||||
const [locale, tz] = tzlocale.split(/\s?,\s?/).map((v) => v.trim());
|
||||
|
||||
if (locale) {
|
||||
try {
|
||||
Intl.DateTimeFormat.supportedLocalesOf(locale);
|
||||
args[0] = locale;
|
||||
} catch (e) {
|
||||
args[0] = undefined;
|
||||
logger.error(`invalid locale provided ${locale}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (tz) {
|
||||
const intlTz = Intl.supportedValuesOf('timeZone').find((v) => v.toLowerCase() === tz.toLowerCase());
|
||||
if (intlTz) args[1] = { timeZone: intlTz };
|
||||
else {
|
||||
args[1] = undefined;
|
||||
logger.error(`invalid timezone provided ${tz}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (mod) {
|
||||
case 'locale':
|
||||
return value.toLocaleString();
|
||||
return value.toLocaleString(...args);
|
||||
case 'time':
|
||||
return value.toLocaleTimeString();
|
||||
return value.toLocaleTimeString(...args);
|
||||
case 'date':
|
||||
return value.toLocaleDateString();
|
||||
return value.toLocaleDateString(...args);
|
||||
case 'unix':
|
||||
return Math.floor(value.getTime() / 1000).toString();
|
||||
case 'iso':
|
||||
@@ -93,6 +128,10 @@ function modifier(mod: string, value: unknown): string {
|
||||
return value.getMinutes().toString();
|
||||
case 'second':
|
||||
return value.getSeconds().toString();
|
||||
case 'ampm':
|
||||
return value.getHours() < 12 ? 'am' : 'pm';
|
||||
case 'AMPM':
|
||||
return value.getHours() < 12 ? 'AM' : 'PM';
|
||||
default:
|
||||
return '{unknown_date_modifier}';
|
||||
}
|
||||
@@ -115,7 +154,7 @@ function modifier(mod: string, value: unknown): string {
|
||||
default:
|
||||
return '{unknown_str_modifier}';
|
||||
}
|
||||
} else if (typeof value === 'number') {
|
||||
} else if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
switch (mod) {
|
||||
case 'comma':
|
||||
return value.toLocaleString();
|
||||
@@ -125,6 +164,8 @@ function modifier(mod: string, value: unknown): string {
|
||||
return value.toString(8);
|
||||
case 'binary':
|
||||
return value.toString(2);
|
||||
case 'bytes':
|
||||
return bytesToHuman(value);
|
||||
default:
|
||||
return '{unknown_int_modifier}';
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { guess } from 'lib/mimes';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createToken, hashPassword } from 'lib/util';
|
||||
import { jsonUserReplacer } from 'lib/utils/client';
|
||||
import { extname } from 'path';
|
||||
|
||||
const logger = Logger.get('user');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
// handle invites
|
||||
if (req.body.code) {
|
||||
if (!config.features.invites) return res.badRequest('invites are disabled');
|
||||
|
||||
const { code, username, password } = req.body as {
|
||||
code?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { code: code ?? '' },
|
||||
});
|
||||
if (!invite && code) return res.badRequest('invalid invite code');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (user) return res.badRequest('username already exists');
|
||||
const hashed = await hashPassword(password);
|
||||
|
||||
let avatar;
|
||||
if (config.features.default_avatar) {
|
||||
logger.debug(`using default avatar ${config.features.default_avatar}`);
|
||||
|
||||
const buf = await readFile(config.features.default_avatar);
|
||||
const mimetype = await guess(extname(config.features.default_avatar));
|
||||
logger.debug(`guessed mimetype ${mimetype} for ${config.features.default_avatar}`);
|
||||
|
||||
avatar = `data:${mimetype};base64,${buf.toString('base64')}`;
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
password: hashed,
|
||||
username,
|
||||
token: createToken(),
|
||||
administrator: false,
|
||||
avatar,
|
||||
},
|
||||
});
|
||||
|
||||
if (code) {
|
||||
await prisma.invite.update({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
data: {
|
||||
used: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`created user via invite ${code} ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
||||
|
||||
logger.info(
|
||||
`Created user ${newUser.username} (${newUser.id}) ${
|
||||
code ? `from invite code ${code}` : 'via registration'
|
||||
}`
|
||||
);
|
||||
|
||||
return res.json({ success: true });
|
||||
}
|
||||
|
||||
const user = await req.user();
|
||||
if (!user) return res.unauthorized('not logged in');
|
||||
if (!user.administrator) return res.forbidden('you arent an administrator');
|
||||
|
||||
const { username, password, administrator } = req.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
administrator: boolean;
|
||||
};
|
||||
|
||||
if (!username) return res.badRequest('no username');
|
||||
if (!password) return res.badRequest('no password');
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
if (existing) return res.badRequest('user exists');
|
||||
|
||||
const hashed = await hashPassword(password);
|
||||
|
||||
let avatar;
|
||||
if (config.features.default_avatar) {
|
||||
logger.debug(`using default avatar ${config.features.default_avatar}`);
|
||||
|
||||
const buf = await readFile(config.features.default_avatar);
|
||||
const mimetype = await guess(extname(config.features.default_avatar));
|
||||
logger.debug(`guessed mimetype ${mimetype} for ${config.features.default_avatar}`);
|
||||
|
||||
avatar = `data:${mimetype};base64,${buf.toString('base64')}`;
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
password: hashed,
|
||||
username,
|
||||
token: createToken(),
|
||||
administrator,
|
||||
avatar,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`created user ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
||||
|
||||
delete newUser.password;
|
||||
|
||||
logger.info(`Created user ${newUser.username} (${newUser.id})`);
|
||||
|
||||
return res.json(newUser);
|
||||
}
|
||||
|
||||
export default withZipline(handler, {
|
||||
methods: ['POST'],
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline';
|
||||
@@ -16,8 +17,12 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const expiry = parseExpiry(expiresAt);
|
||||
if (!expiry) return res.badRequest('invalid date');
|
||||
let expiry: Date;
|
||||
try {
|
||||
expiry = parseExpiry(expiresAt);
|
||||
} catch (error) {
|
||||
return res.badRequest(error.message);
|
||||
}
|
||||
const counts = count ? count : 1;
|
||||
|
||||
if (counts > 1) {
|
||||
@@ -37,7 +42,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
logger.info(
|
||||
`${user.username} (${user.id}) created ${data.length} invites with codes ${data
|
||||
.map((invite) => invite.code)
|
||||
.join(', ')}`
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
return res.json(data);
|
||||
@@ -60,19 +65,22 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
const { code } = req.query as { code: string };
|
||||
if (!code) return res.badRequest('no code');
|
||||
|
||||
const invite = await prisma.invite.delete({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const invite = await prisma.invite.delete({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) return res.notFound('invite not found');
|
||||
logger.debug(`deleted invite ${JSON.stringify(invite)}`);
|
||||
|
||||
logger.debug(`deleted invite ${JSON.stringify(invite)}`);
|
||||
logger.info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
||||
|
||||
logger.info(`${user.username} (${user.id}) deleted invite ${invite.code}`);
|
||||
|
||||
return res.json(invite);
|
||||
return res.json(invite);
|
||||
} catch (error) {
|
||||
if (error instanceof PrismaClientKnownRequestError) return res.notFound('invite not found');
|
||||
else throw error;
|
||||
}
|
||||
} else {
|
||||
const invites = await prisma.invite.findMany({
|
||||
orderBy: {
|
||||
|
||||
@@ -14,8 +14,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
code?: string;
|
||||
};
|
||||
|
||||
const users = await prisma.user.findMany();
|
||||
if (users.length === 0) {
|
||||
const users = await prisma.user.count();
|
||||
if (users === 0) {
|
||||
logger.debug('no users found... creating default user...');
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
@@ -51,7 +51,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
const success = verify_totp_code(user.totpSecret, code);
|
||||
logger.debug(
|
||||
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${code}) => ${success}`
|
||||
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${code}) => ${success}`,
|
||||
);
|
||||
if (!success) return res.badRequest('Invalid code', { totp: true });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { discord_auth } from 'lib/oauth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
|
||||
|
||||
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
@@ -12,7 +12,10 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
error: 'oauth registration is disabled',
|
||||
};
|
||||
|
||||
if (!notNull(config.oauth.discord_client_id, config.oauth.discord_client_secret)) {
|
||||
if (
|
||||
!isNotNullOrUndefined(config.oauth.discord_client_id) &&
|
||||
!isNotNullOrUndefined(config.oauth.discord_client_secret)
|
||||
) {
|
||||
logger.error('Discord OAuth is not configured');
|
||||
|
||||
return {
|
||||
@@ -26,7 +29,8 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
redirect: discord_auth.oauth_url(
|
||||
config.oauth.discord_client_id,
|
||||
`${config.core.return_https ? 'https' : 'http'}://${host}`,
|
||||
state
|
||||
state,
|
||||
config.oauth.discord_redirect_uri,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -35,7 +39,9 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
client_secret: config.oauth.discord_client_secret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
|
||||
redirect_uri:
|
||||
config.oauth.discord_redirect_uri ||
|
||||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
|
||||
scope: 'identify',
|
||||
});
|
||||
|
||||
@@ -67,6 +73,12 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
: `https://cdn.discordapp.com/embed/avatars/${userJson.discriminator % 5}.png`;
|
||||
const avatarBase64 = await getBase64URLFromURL(avatar);
|
||||
|
||||
if (
|
||||
config.oauth.discord_whitelisted_users?.length &&
|
||||
!config.oauth.discord_whitelisted_users.includes(userJson.id)
|
||||
)
|
||||
return { error: 'user is not whitelisted' };
|
||||
|
||||
return {
|
||||
username: userJson.username,
|
||||
user_id: userJson.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { github_auth } from 'lib/oauth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
|
||||
|
||||
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
@@ -12,7 +12,10 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
|
||||
error: 'oauth registration is disabled',
|
||||
};
|
||||
|
||||
if (!notNull(config.oauth.github_client_id, config.oauth.github_client_secret)) {
|
||||
if (
|
||||
!isNotNullOrUndefined(config.oauth.github_client_id) &&
|
||||
!isNotNullOrUndefined(config.oauth.github_client_secret)
|
||||
) {
|
||||
logger.error('GitHub OAuth is not configured');
|
||||
return {
|
||||
error_code: 401,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Logger from 'lib/logger';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
|
||||
import { withZipline } from 'lib/middleware/withZipline';
|
||||
import { google_auth } from 'lib/oauth';
|
||||
import { getBase64URLFromURL, notNull } from 'lib/util';
|
||||
import { getBase64URLFromURL, isNotNullOrUndefined } from 'lib/util';
|
||||
|
||||
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauth_registration)
|
||||
@@ -12,7 +12,10 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
error: 'oauth registration is disabled',
|
||||
};
|
||||
|
||||
if (!notNull(config.oauth.google_client_id, config.oauth.google_client_secret)) {
|
||||
if (
|
||||
!isNotNullOrUndefined(config.oauth.google_client_id) &&
|
||||
!isNotNullOrUndefined(config.oauth.google_client_secret)
|
||||
) {
|
||||
logger.error('Google OAuth is not configured');
|
||||
return {
|
||||
error_code: 401,
|
||||
@@ -25,7 +28,8 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
redirect: google_auth.oauth_url(
|
||||
config.oauth.google_client_id,
|
||||
`${config.core.return_https ? 'https' : 'http'}://${host}`,
|
||||
state
|
||||
state,
|
||||
config.oauth.google_redirect_uri,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -33,7 +37,9 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
|
||||
code,
|
||||
client_id: config.oauth.google_client_id,
|
||||
client_secret: config.oauth.google_client_secret,
|
||||
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
|
||||
redirect_uri:
|
||||
config.oauth.google_redirect_uri ||
|
||||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
|
||||
|
||||
@@ -11,23 +11,50 @@ import { extname } from 'path';
|
||||
const logger = Logger.get('user');
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (!config.features.user_registration) return res.badRequest('user registration is disabled');
|
||||
const user = await req.user();
|
||||
let badRequest,
|
||||
usedInvite = false;
|
||||
|
||||
const { username, password, administrator } = req.body as {
|
||||
if (!config.features.user_registration && !config.features.invites && !user?.administrator)
|
||||
return res.badRequest('This endpoint is unavailable due to current configurations');
|
||||
else if (!!user && !user?.administrator) return res.badRequest('Already logged in');
|
||||
|
||||
const { username, password, administrator, code } = req.body as {
|
||||
username: string;
|
||||
password: string;
|
||||
administrator: boolean;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
if (!username) return res.badRequest('no username');
|
||||
if (!password) return res.badRequest('no password');
|
||||
if (!username) badRequest = true;
|
||||
if (!password) badRequest = true;
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
if (existing) return res.badRequest('user exists');
|
||||
|
||||
if (existing) badRequest = true;
|
||||
|
||||
if (badRequest) return res.badRequest('Bad Username/Password');
|
||||
|
||||
if (code) {
|
||||
if (config.features.invites) {
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite || invite?.used) return res.badRequest('Bad invite');
|
||||
usedInvite = true;
|
||||
} else return res.badRequest('Bad Username/Password');
|
||||
} else if (config.features.invites && !config.features.user_registration && !user?.administrator)
|
||||
return res.badRequest('Bad invite');
|
||||
|
||||
const hashed = await hashPassword(password);
|
||||
|
||||
@@ -47,12 +74,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
password: hashed,
|
||||
username,
|
||||
token: createToken(),
|
||||
administrator,
|
||||
administrator: user?.superAdmin ? administrator : false,
|
||||
avatar,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`registered user ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
||||
if (usedInvite)
|
||||
await prisma.invite.update({
|
||||
where: { code },
|
||||
data: { used: true },
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`registered user${usedInvite ? ' via invite ' + code : ''} ${JSON.stringify(newUser, jsonUserReplacer)}`,
|
||||
);
|
||||
|
||||
delete newUser.password;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (!image) return res.notFound('image not found');
|
||||
|
||||
logger.info(
|
||||
`${user.username} (${user.id}) requested to read exif metadata for image ${image.name} (${image.id})`
|
||||
`${user.username} (${user.id}) requested to read exif metadata for image ${image.name} (${image.id})`,
|
||||
);
|
||||
|
||||
if (config.datasource.type === 'local') {
|
||||
|
||||
@@ -54,7 +54,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
logger.debug(`shortened ${JSON.stringify(url)}`);
|
||||
|
||||
logger.info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
||||
logger.info(`User ${user.username} (${user.id}) shortened a url ${url.destination} (${url.id})`);
|
||||
|
||||
let domain;
|
||||
if (req.headers['override-domain']) {
|
||||
|
||||
@@ -42,6 +42,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
expiresAt?: Date;
|
||||
removed_gps?: boolean;
|
||||
assumed_mimetype?: string | boolean;
|
||||
folder?: number;
|
||||
} = {
|
||||
files: [],
|
||||
};
|
||||
@@ -49,16 +50,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
let expiry: Date;
|
||||
|
||||
if (expiresAt) {
|
||||
expiry = parseExpiry(expiresAt);
|
||||
if (!expiry) return res.badRequest('invalid date');
|
||||
else {
|
||||
try {
|
||||
expiry = parseExpiry(expiresAt);
|
||||
response.expiresAt = expiry;
|
||||
} catch (error) {
|
||||
return res.badRequest(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (zconfig.uploader.default_expiration) {
|
||||
expiry = parseExpiry(zconfig.uploader.default_expiration);
|
||||
if (!expiry) return res.badRequest('invalid date (UPLOADER_DEFAULT_EXPIRATION)');
|
||||
try {
|
||||
expiry = parseExpiry(zconfig.uploader.default_expiration);
|
||||
} catch (error) {
|
||||
return res.badRequest(`${error.message} (UPLOADER_DEFAULT_EXPIRATION)`);
|
||||
}
|
||||
}
|
||||
|
||||
const rawFormat = ((req.headers['format'] as string) || zconfig.uploader.default_format).toLowerCase();
|
||||
@@ -78,8 +83,32 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (isNaN(fileMaxViews)) return res.badRequest('invalid max views (invalid number)');
|
||||
if (fileMaxViews < 0) return res.badRequest('invalid max views (max views < 0)');
|
||||
|
||||
const folderToAdd = req.headers['x-zipline-folder'] ? Number(req.headers['x-zipline-folder']) : null;
|
||||
if (folderToAdd) {
|
||||
if (isNaN(folderToAdd)) return res.badRequest('invalid folder id (invalid number)');
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folderToAdd,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
if (!folder) return res.badRequest('invalid folder id (no folder found)');
|
||||
|
||||
response.folder = folder.id;
|
||||
}
|
||||
|
||||
// handle partial uploads before ratelimits
|
||||
if (req.headers['content-range'] && zconfig.chunks.enabled) {
|
||||
if (format === 'name') {
|
||||
const existing = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: req.headers['x-zipline-partial-filename'] as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) return res.badRequest('filename already exists (conflict: NAME format)');
|
||||
}
|
||||
|
||||
// parses content-range header (bytes start-end/total)
|
||||
const [start, end, total] = req.headers['content-range']
|
||||
.replace('bytes ', '')
|
||||
@@ -101,7 +130,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
start,
|
||||
end,
|
||||
total,
|
||||
})}`
|
||||
})}`,
|
||||
);
|
||||
|
||||
const tempFile = join(zconfig.core.temp_directory, `zipline_partial_${identifier}_${start}_${end}`);
|
||||
@@ -118,6 +147,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
mimetype: req.headers.uploadtext ? 'text/plain' : mimetype,
|
||||
userId: user.id,
|
||||
originalName: req.headers['original-name'] ? filename ?? null : null,
|
||||
...(folderToAdd && {
|
||||
folderId: folderToAdd,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -193,29 +225,32 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
mimetype: x.mimetype,
|
||||
size: x.size,
|
||||
encoding: x.encoding,
|
||||
}))
|
||||
)}`
|
||||
})),
|
||||
)}`,
|
||||
);
|
||||
|
||||
for (let i = 0; i !== req.files.length; ++i) {
|
||||
const file = req.files[i];
|
||||
|
||||
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit'])
|
||||
return res.badRequest(`file[${i}]: size too big`);
|
||||
if (!file.originalname) return res.badRequest(`file[${i}]: no filename`);
|
||||
|
||||
const ext = file.originalname.split('.').length === 1 ? '' : file.originalname.split('.').pop();
|
||||
const decodedName = decodeURI(file.originalname);
|
||||
|
||||
const ext = decodedName.split('.').length === 1 ? '' : decodedName.split('.').pop();
|
||||
if (zconfig.uploader.disabled_extensions.includes(ext))
|
||||
return res.badRequest(`file[${i}]: disabled extension recieved: ${ext}`);
|
||||
let fileName = await formatFileName(format, file.originalname);
|
||||
const fileName = await formatFileName(format, decodedName);
|
||||
|
||||
if (req.headers['x-zipline-filename']) {
|
||||
fileName = req.headers['x-zipline-filename'] as string;
|
||||
if (format === 'name' || req.headers['x-zipline-filename']) {
|
||||
const exist = (req.headers['x-zipline-filename'] as string) || decodedName;
|
||||
const existing = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: fileName,
|
||||
name: exist,
|
||||
},
|
||||
});
|
||||
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${fileName}'`);
|
||||
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${decodedName}'`);
|
||||
}
|
||||
|
||||
let password = null;
|
||||
@@ -226,7 +261,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
let mimetype = file.mimetype;
|
||||
|
||||
if (file.mimetype === 'application/octet-stream' && zconfig.uploader.assume_mimetypes) {
|
||||
const ext = parse(file.originalname).ext.replace('.', '');
|
||||
const ext = parse(decodedName).ext.replace('.', '');
|
||||
const mime = await guess(ext);
|
||||
|
||||
if (!mime) response.assumed_mimetype = false;
|
||||
@@ -247,8 +282,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
password,
|
||||
expiresAt: expiry,
|
||||
maxViews: fileMaxViews,
|
||||
originalName: req.headers['original-name'] ? file.originalname ?? null : null,
|
||||
originalName: req.headers['original-name'] ? decodedName ?? null : null,
|
||||
size: file.size,
|
||||
...(folderToAdd && {
|
||||
folderId: folderToAdd,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -257,12 +295,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
if (compressionUsed) {
|
||||
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
|
||||
await datasource.save(fileUpload.name, buffer);
|
||||
await datasource.save(fileUpload.name, buffer, { type: 'image/jpeg' });
|
||||
logger.info(
|
||||
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`
|
||||
`User ${user.username} (${user.id}) compressed image from ${file.buffer.length} -> ${buffer.length} bytes`,
|
||||
);
|
||||
} else {
|
||||
await datasource.save(fileUpload.name, file.buffer);
|
||||
await datasource.save(fileUpload.name, file.buffer, { type: file.mimetype });
|
||||
}
|
||||
|
||||
logger.info(`User ${user.username} (${user.id}) uploaded ${fileUpload.name} (${fileUpload.id})`);
|
||||
@@ -286,7 +324,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
user,
|
||||
fileUpload,
|
||||
`${domain}/r/${invis ? invis.invis : encodeURI(fileUpload.name)}`,
|
||||
responseUrl
|
||||
responseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
promises.push(
|
||||
prisma.user.delete({
|
||||
where: { id: target.id },
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
if (req.body.delete_files) {
|
||||
@@ -61,7 +61,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
where: {
|
||||
userId: target.id,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
Promise.all(promises).then((promised) => {
|
||||
@@ -71,10 +71,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
req.body.delete_files
|
||||
? logger.info(
|
||||
`User ${user.username} (${user.id}) deleted ${count} files of user ${newTarget.username} (${newTarget.id})`
|
||||
`User ${user.username} (${user.id}) deleted ${count} files of user ${newTarget.username} (${newTarget.id})`,
|
||||
)
|
||||
: logger.info(
|
||||
`User ${user.username} (${user.id}) deleted user ${newTarget.username} (${newTarget.id})`
|
||||
`User ${user.username} (${user.id}) deleted user ${newTarget.username} (${newTarget.id})`,
|
||||
);
|
||||
|
||||
delete newTarget.password;
|
||||
@@ -177,7 +177,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
logger.debug(`updated user ${id} with ${JSON.stringify(newUser, jsonUserReplacer)}`);
|
||||
|
||||
logger.info(
|
||||
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`
|
||||
`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`,
|
||||
);
|
||||
|
||||
delete newUser.password;
|
||||
|
||||
@@ -3,18 +3,19 @@ import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (!config.features.user_registration && !req.body.code)
|
||||
return res.badRequest('user registration is disabled');
|
||||
else if (!config.features.invites && req.body.code) return res.forbidden('user/invites are disabled');
|
||||
const { code, username } = req.body as { code?: string; username?: string };
|
||||
|
||||
if (!req.body?.code) return res.badRequest('no code');
|
||||
if (!req.body?.username) return res.badRequest('no username');
|
||||
if (!config.features.user_registration && !code) return res.badRequest('user registration is disabled');
|
||||
else if (!config.features.invites && code) return res.forbidden('user invites are disabled');
|
||||
|
||||
const { code, username } = req.body as { code: string; username: string };
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
if (!invite) return res.badRequest('invalid invite code');
|
||||
if (config.features.invites && !code) return res.badRequest('no code');
|
||||
else if (config.features.invites && code) {
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
if (!invite) return res.badRequest('invalid invite code');
|
||||
}
|
||||
if (!username) return res.badRequest('no username');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username },
|
||||
|
||||
@@ -77,7 +77,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
write_stream.close();
|
||||
logger.debug(`finished writing zip to ${path} at ${data.length} bytes written`);
|
||||
logger.info(
|
||||
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`
|
||||
`Export for ${user.username} (${user.id}) has completed and is available at ${export_name}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -76,7 +76,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (file.thumbnail?.name) await datasource.delete(file.thumbnail.name);
|
||||
|
||||
logger.info(
|
||||
`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id}) owned by ${file.user.username} (${file.user.id})`
|
||||
`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id}) owned by ${file.user.username} (${file.user.id})`,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
@@ -139,7 +139,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
expiresAt: Date;
|
||||
maxViews: number;
|
||||
views: number;
|
||||
size: number;
|
||||
size: bigint;
|
||||
originalName: string;
|
||||
thumbnail?: { name: string };
|
||||
}[] = await prisma.file.findMany({
|
||||
|
||||
@@ -83,7 +83,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
logger.debug(`added file ${fileIdParsed} to folder ${idParsed}`);
|
||||
|
||||
logger.info(
|
||||
`Added file "${file.name}" to folder "${folder.name}" for user ${user.username} (${user.id})`
|
||||
`Added file "${file.name}" to folder "${folder.name}" for user ${user.username} (${user.id})`,
|
||||
);
|
||||
|
||||
if (req.query.files) {
|
||||
@@ -94,7 +94,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[i].name
|
||||
folder.files[i].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[i].name
|
||||
folder.files[i].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
logger.debug(`removed file ${fileIdParsed} from folder ${idParsed}`);
|
||||
|
||||
logger.info(
|
||||
`Removed file "${file.name}" from folder "${folder.name}" for user ${user.username} (${user.id})`
|
||||
`Removed file "${file.name}" from folder "${folder.name}" for user ${user.username} (${user.id})`,
|
||||
);
|
||||
|
||||
if (req.query.files) {
|
||||
@@ -224,7 +224,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[i].name
|
||||
folder.files[i].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[i].name
|
||||
folder.files[i].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
if (files.length !== add.length)
|
||||
return res.badRequest(
|
||||
`files ${add.filter((id) => !files.find((file) => file.id === Number(id))).join(', ')} not found`
|
||||
`files ${add.filter((id) => !files.find((file) => file.id === Number(id))).join(', ')} not found`,
|
||||
);
|
||||
|
||||
const folder = await prisma.folder.create({
|
||||
@@ -87,7 +87,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
|
||||
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[j].name
|
||||
folder.files[j].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: discord_auth.oauth_url(
|
||||
zconfig.oauth.discord_client_id,
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -60,7 +60,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: discord_auth.oauth_url(
|
||||
zconfig.oauth.discord_client_id,
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -80,7 +80,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
const resp = await fetch(
|
||||
`https://people.googleapis.com/v1/people/me?access_token=${
|
||||
user.oauth.find((o) => o.provider === 'GOOGLE').token
|
||||
}&personFields=names,photos`
|
||||
}&personFields=names,photos`,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const provider = user.oauth.find((o) => o.provider === 'GOOGLE');
|
||||
@@ -91,7 +91,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: google_auth.oauth_url(
|
||||
zconfig.oauth.google_client_id,
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -114,7 +114,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
error: 'oauth token expired',
|
||||
redirect_uri: google_auth.oauth_url(
|
||||
zconfig.oauth.google_client_id,
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
|
||||
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
logger.debug(
|
||||
`body(${JSON.stringify(req.body)}): verify_totp_code(${user.totpSecret}, ${
|
||||
req.body.code
|
||||
}) => ${success}`
|
||||
}) => ${success}`,
|
||||
);
|
||||
|
||||
if (!success) return res.badRequest('Invalid code');
|
||||
|
||||
@@ -15,7 +15,7 @@ const sortByValidator = s.enum(
|
||||
'size',
|
||||
'name',
|
||||
'mimetype',
|
||||
] satisfies (keyof Prisma.FileOrderByWithRelationInput)[])
|
||||
] satisfies (keyof Prisma.FileOrderByWithRelationInput)[]),
|
||||
);
|
||||
|
||||
const orderValidator = s.enum('asc', 'desc');
|
||||
@@ -83,7 +83,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
maxViews: number;
|
||||
views: number;
|
||||
folderId: number;
|
||||
size: number;
|
||||
size: bigint;
|
||||
password: string | boolean;
|
||||
thumbnail?: { name: string };
|
||||
}[] = await prisma.file.findMany({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
import config from 'lib/config';
|
||||
import Logger from 'lib/logger';
|
||||
import prisma from 'lib/prisma';
|
||||
@@ -8,15 +9,22 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
if (req.method === 'DELETE') {
|
||||
if (!req.body.id) return res.badRequest('no url id');
|
||||
|
||||
const url = await prisma.url.delete({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const url = await prisma.url.delete({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
Logger.get('url').info(`User ${user.username} (${user.id}) deleted a url ${url.destination} (${url.id})`);
|
||||
Logger.get('url').info(
|
||||
`User ${user.username} (${user.id}) deleted a url ${url.destination} (${url.id})`,
|
||||
);
|
||||
|
||||
return res.json(url);
|
||||
return res.json(url);
|
||||
} catch (err) {
|
||||
if (err instanceof PrismaClientKnownRequestError) return res.notFound('url not found');
|
||||
else throw err;
|
||||
}
|
||||
} else {
|
||||
const urls = await prisma.url.findMany({
|
||||
where: {
|
||||
@@ -38,7 +46,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
||||
for (let i = 0; i !== urls.length; ++i) {
|
||||
(urls[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.urls.route,
|
||||
urls[i].vanity ?? urls[i].id
|
||||
urls[i].vanity ?? urls[i].id,
|
||||
);
|
||||
}
|
||||
return res.json(urls);
|
||||
|
||||
@@ -7,7 +7,7 @@ async function handler(_: NextApiReq, res: NextApiRes) {
|
||||
|
||||
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
|
||||
|
||||
const re = await fetch('https://zipline.diced.tech/api/version?c=' + pkg.version);
|
||||
const re = await fetch('https://zipline.diced.sh/api/version?c=' + pkg.version);
|
||||
const json = await re.json();
|
||||
|
||||
let updateToType = 'stable';
|
||||
|
||||
@@ -6,14 +6,13 @@ import useFetch from 'hooks/useFetch';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { randomChars } from 'lib/util';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export default function Register({ code, title, user_registration }) {
|
||||
export default function Register({ code = undefined, title, user_registration }) {
|
||||
const [active, setActive] = useState(0);
|
||||
const [username, setUsername] = useState('');
|
||||
const [usernameError, setUsernameError] = useState('');
|
||||
@@ -51,7 +50,7 @@ export default function Register({ code, title, user_registration }) {
|
||||
};
|
||||
|
||||
const createUser = async () => {
|
||||
const res = await useFetch(`/api/auth/${user_registration ? 'register' : 'create'}`, 'POST', {
|
||||
const res = await useFetch('/api/auth/register', 'POST', {
|
||||
code: user_registration ? null : code,
|
||||
username,
|
||||
password,
|
||||
@@ -196,20 +195,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
notFound: true,
|
||||
};
|
||||
|
||||
const code = randomChars(4);
|
||||
const temp = await prisma.invite.create({
|
||||
data: {
|
||||
code,
|
||||
createdById: 1,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`request to access user registration, creating temporary invite ${JSON.stringify(temp)}`);
|
||||
|
||||
return {
|
||||
props: {
|
||||
title: config.website.title,
|
||||
code,
|
||||
user_registration: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,9 @@ export default function UploadPage(props) {
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<Layout props={props}>
|
||||
<File chunks={{ chunks_size: props.chunks_size, max_size: props.max_size }} />
|
||||
<File
|
||||
chunks={{ chunks_size: props.chunks_size, max_size: props.max_size, enabled: props.chunks_enabled }}
|
||||
/>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ type LimitedFolder = {
|
||||
createdAt: Date | string;
|
||||
mimetype: string;
|
||||
views: number;
|
||||
size: bigint;
|
||||
}[];
|
||||
user: {
|
||||
username: string;
|
||||
@@ -83,6 +84,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
||||
views: true,
|
||||
createdAt: true,
|
||||
password: true,
|
||||
size: true,
|
||||
thumbnail: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
@@ -101,9 +109,12 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
||||
for (let j = 0; j !== folder.files.length; ++j) {
|
||||
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[j].name
|
||||
folder.files[j].name,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
folder.files[j].size = Number(folder.files[j].size);
|
||||
|
||||
// @ts-ignore
|
||||
if (folder.files[j].password) folder.files[j].password = true;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
||||
import { Box, Button, Modal, PasswordInput, Title } from '@mantine/core';
|
||||
import type { File, Thumbnail } from '@prisma/client';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import exts from 'lib/exts';
|
||||
@@ -27,13 +27,18 @@ export default function EmbeddedFile({
|
||||
host: string;
|
||||
compress?: boolean;
|
||||
}) {
|
||||
const dataURL = (route: string) => `${route}/${encodeURI(file.name)}?compress=${compress ?? false}`;
|
||||
const dataURL = (route: string, pass?: string) =>
|
||||
`${route}/${encodeURIComponent(file.name)}?compress=${compress ?? false}${
|
||||
pass ? `&password=${encodeURIComponent(pass)}` : ''
|
||||
}`;
|
||||
|
||||
const router = useRouter();
|
||||
const [opened, setOpened] = useState(pass);
|
||||
const [opened, setOpened] = useState(pass || !!file.password);
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [downloadWPass, setDownloadWPass] = useState(false);
|
||||
|
||||
// reapply date from workaround
|
||||
file.createdAt = new Date(file ? file.createdAt : 0);
|
||||
|
||||
@@ -45,21 +50,28 @@ export default function EmbeddedFile({
|
||||
if (prismRender) return router.push(`/code/${file.name}?password=${password}`);
|
||||
updateImage(`/api/auth/image?id=${file.id}&password=${password}`);
|
||||
setOpened(false);
|
||||
setDownloadWPass(true);
|
||||
} else {
|
||||
setError('Invalid password');
|
||||
}
|
||||
};
|
||||
|
||||
const updateImage = async (url?: string) => {
|
||||
if (!file.mimetype.startsWith('image')) return;
|
||||
|
||||
const imageEl = document.getElementById('image_content') as HTMLImageElement;
|
||||
|
||||
const img = new Image();
|
||||
img.addEventListener('load', function () {
|
||||
if (this.naturalWidth > innerWidth)
|
||||
imageEl.width = Math.floor(
|
||||
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth)
|
||||
);
|
||||
else imageEl.width = this.naturalWidth;
|
||||
// my best attempt of recreating https://searchfox.org/mozilla-central/source/dom/html/ImageDocument.cpp#271-276
|
||||
// and it actually works
|
||||
|
||||
const ratio = Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth);
|
||||
const newWidth = Math.max(1, Math.floor(ratio * this.naturalWidth));
|
||||
const newHeight = Math.max(1, Math.floor(ratio * this.naturalHeight));
|
||||
|
||||
imageEl.width = newWidth;
|
||||
imageEl.height = newHeight;
|
||||
});
|
||||
|
||||
img.src = url || dataURL('/r');
|
||||
@@ -72,11 +84,19 @@ export default function EmbeddedFile({
|
||||
useEffect(() => {
|
||||
if (pass) {
|
||||
setOpened(true);
|
||||
} else {
|
||||
updateImage();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file?.mimetype?.startsWith('image')) return;
|
||||
|
||||
updateImage();
|
||||
window.addEventListener('resize', () => updateImage());
|
||||
return () => {
|
||||
window.removeEventListener('resize', () => updateImage());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -116,8 +136,6 @@ export default function EmbeddedFile({
|
||||
<meta name='twitter:card' content='player' />
|
||||
<meta name='twitter:player' content={`${host}/r/${file.name}`} />
|
||||
<meta name='twitter:player:stream' content={`${host}/r/${file.name}`} />
|
||||
<meta name='twitter:player:width' content='720' />
|
||||
<meta name='twitter:player:height' content='480' />
|
||||
<meta name='twitter:player:stream:content_type' content={file.mimetype} />
|
||||
<meta name='twitter:title' content={file.name} />
|
||||
|
||||
@@ -128,13 +146,12 @@ export default function EmbeddedFile({
|
||||
</>
|
||||
)}
|
||||
|
||||
<meta property='og:type' content={'video.other'} />
|
||||
<meta property='og:url' content={`${host}/r/${file.name}`} />
|
||||
<meta property='og:video' content={`${host}/r/${file.name}`} />
|
||||
<meta property='og:video:url' content={`${host}/r/${file.name}`} />
|
||||
<meta property='og:video:secure_url' content={`${host}/r/${file.name}`} />
|
||||
<meta property='og:video:type' content={file.mimetype} />
|
||||
<meta property='og:video:width' content='720' />
|
||||
<meta property='og:video:height' content='480' />
|
||||
</>
|
||||
)}
|
||||
{file.mimetype.startsWith('audio') && (
|
||||
@@ -162,14 +179,13 @@ export default function EmbeddedFile({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title='Password Protected'
|
||||
title={<Title order={3}>Password Protected</Title>}
|
||||
centered={true}
|
||||
withCloseButton={true}
|
||||
withCloseButton={false}
|
||||
closeOnEscape={false}
|
||||
closeOnClickOutside={false}
|
||||
>
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
placeholder='Password'
|
||||
error={error}
|
||||
value={password}
|
||||
@@ -192,7 +208,17 @@ export default function EmbeddedFile({
|
||||
)}
|
||||
|
||||
{file.mimetype.startsWith('video') && (
|
||||
<video src={dataURL('/r')} controls autoPlay muted id='video_content' />
|
||||
<video
|
||||
style={{
|
||||
maxHeight: '100vh',
|
||||
maxWidth: '100vw',
|
||||
}}
|
||||
src={dataURL('/r')}
|
||||
controls
|
||||
autoPlay
|
||||
muted
|
||||
id='video_content'
|
||||
/>
|
||||
)}
|
||||
|
||||
{file.mimetype.startsWith('audio') && (
|
||||
@@ -202,7 +228,7 @@ export default function EmbeddedFile({
|
||||
{!file.mimetype.startsWith('video') &&
|
||||
!file.mimetype.startsWith('image') &&
|
||||
!file.mimetype.startsWith('audio') && (
|
||||
<AnchorNext component={Link} href={dataURL('/r')}>
|
||||
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
|
||||
Can't preview this file. Click here to download it.
|
||||
</AnchorNext>
|
||||
)}
|
||||
@@ -225,6 +251,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
let host = context.req.headers.host;
|
||||
if (!file) return { notFound: true };
|
||||
|
||||
// @ts-ignore
|
||||
file.size = Number(file.size);
|
||||
|
||||
const proto = context.req.headers['x-forwarded-proto'];
|
||||
try {
|
||||
if (
|
||||
|
||||
@@ -12,7 +12,7 @@ async function main() {
|
||||
}
|
||||
|
||||
const files = (await readdir(temp)).filter(
|
||||
(x) => x.startsWith('zipline_partial_') || x.startsWith('zipline_thumb_')
|
||||
(x) => x.startsWith('zipline_partial_') || x.startsWith('zipline_thumb_'),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
console.log('No partial files found, exiting..');
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { readdir, readFile } from 'fs/promises';
|
||||
import { statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import config from 'lib/config';
|
||||
import datasource from 'lib/datasource';
|
||||
import { guess } from 'lib/mimes';
|
||||
import { migrations } from 'server/util';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
|
||||
async function main() {
|
||||
const directory = process.argv[2];
|
||||
@@ -25,13 +27,16 @@ async function main() {
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const mime = await guess(files[i].split('.').pop());
|
||||
const { size } = statSync(join(directory, files[i]));
|
||||
|
||||
data.push({
|
||||
data[i] = {
|
||||
name: files[i],
|
||||
mimetype: mime,
|
||||
userId,
|
||||
});
|
||||
console.log(`Imported ${files[i]} (${mime} mimetype) to user ${userId}`);
|
||||
size,
|
||||
};
|
||||
|
||||
console.log(`Imported ${files[i]} (${bytesToHuman(size)}) (${mime} mimetype) to user ${userId}`);
|
||||
}
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
@@ -49,7 +54,9 @@ async function main() {
|
||||
console.log(`Copying files to ${config.datasource.type} storage..`);
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
await datasource.save(file, await readFile(join(directory, file)));
|
||||
await datasource.save(file, await readFile(join(directory, file)), {
|
||||
type: data[i]?.mimetype ?? 'application/octet-stream',
|
||||
});
|
||||
}
|
||||
console.log(`Finished copying files to ${config.datasource.type} storage.`);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ async function main() {
|
||||
|
||||
notFound
|
||||
? console.log(
|
||||
'At least one file has been found to not exist in the datasource but was on the database. To remove these files, run the script with the --force-delete flag.'
|
||||
'At least one file has been found to not exist in the datasource but was on the database. To remove these files, run the script with the --force-delete flag.',
|
||||
)
|
||||
: console.log('Done.');
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
|
||||
this.header('Content-Length', size);
|
||||
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
|
||||
this.header('Content-Disposition', `inline; filename="${file.originalName || file.name}"`);
|
||||
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
|
||||
|
||||
return this.send(data);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
function postUrlDecorator(fastify: FastifyInstance, _, done) {
|
||||
fastify.decorateReply('postUrl', postUrl);
|
||||
fastify.decorateReply('postUrl', postUrl.bind(fastify));
|
||||
done();
|
||||
|
||||
async function postUrl(this: FastifyReply, url: Url) {
|
||||
|
||||
@@ -11,7 +11,7 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
done();
|
||||
|
||||
async function rawFile(this: FastifyReply, id: string) {
|
||||
const { download, compress } = this.request.query as { download?: string; compress?: boolean };
|
||||
const { download, compress = 'false' } = this.request.query as { download?: string; compress?: string };
|
||||
|
||||
const data = await this.server.datasource.get(id);
|
||||
if (!data) return this.notFound();
|
||||
@@ -22,11 +22,11 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
|
||||
if (
|
||||
this.server.config.core.compression.enabled &&
|
||||
compress &&
|
||||
compress?.match(/^true$/i) &&
|
||||
!this.request.headers['X-Zipline-NoCompress'] &&
|
||||
!!this.request.headers['accept-encoding']
|
||||
)
|
||||
if (size > this.server.config.core.compression.threshold)
|
||||
if (size > this.server.config.core.compression.threshold && mimetype.match(/^(image|video|text)/))
|
||||
return this.send(useCompress.call(this, data));
|
||||
this.header('Content-Length', size);
|
||||
return this.send(data);
|
||||
|
||||
@@ -28,6 +28,12 @@ const logger = Logger.get('server');
|
||||
|
||||
const server = fastify(genFastifyOpts());
|
||||
|
||||
// Normally I would never condone this, but I lack the patience to deal with this correctly.
|
||||
// This is just to get JSON.stringify to globally serialize BigInt's
|
||||
BigInt.prototype['toJSON'] = function () {
|
||||
return Number(this);
|
||||
};
|
||||
|
||||
if (dev) {
|
||||
server.addHook('onRoute', (opts) => {
|
||||
logger.child('route').debug(JSON.stringify(opts));
|
||||
@@ -87,7 +93,7 @@ async function start() {
|
||||
url: req.url,
|
||||
headers: req.headers,
|
||||
body: req.headers['content-type']?.startsWith('application/json') ? req.body : undefined,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,7 +185,7 @@ Disallow: ${config.urls.route}
|
||||
.info(
|
||||
`started ${dev ? 'development' : 'production'} zipline@${version} server${
|
||||
config.features.headless ? ' (headless)' : ''
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
|
||||
await clearInvites.bind(server)();
|
||||
@@ -259,10 +265,8 @@ async function thumbs(this: FastifyInstance) {
|
||||
new Worker('./dist/worker/thumbnail.js', {
|
||||
workerData: {
|
||||
videos: chunk,
|
||||
config,
|
||||
datasource,
|
||||
},
|
||||
});
|
||||
}).on('error', (err) => logger.child('thumbnail').error(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
|
||||
fastify.logger
|
||||
.error('Secret is not set!')
|
||||
.error(
|
||||
'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!'
|
||||
'Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!',
|
||||
)
|
||||
.error('Please change your secret in the config file or environment variables.')
|
||||
.error(
|
||||
'The config file is located at `.env.local`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.'
|
||||
'The config file is located at `.env.local`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.',
|
||||
)
|
||||
.error(
|
||||
'It is recomended to use a secret that is alphanumeric and randomized. If you include special characters, surround the secret with quotes.'
|
||||
'It is recomended to use a secret that is alphanumeric and randomized. If you include special characters, surround the secret with quotes.',
|
||||
)
|
||||
.error('A way you can generate this is through a password manager you may have.');
|
||||
|
||||
@@ -41,7 +41,7 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
|
||||
.error("Found temporary files in Zipline's temp directory.")
|
||||
.error('This can happen if Zipline crashes or is stopped while chunking a file.')
|
||||
.error(
|
||||
'If you are sure that no files are currently being processed, you can delete the files in the temp directory.'
|
||||
'If you are sure that no files are currently being processed, you can delete the files in the temp directory.',
|
||||
)
|
||||
.error('The temp directory is located at: ' + config.core.temp_directory)
|
||||
.error('If you are unsure, you can safely ignore this message.');
|
||||
|
||||
@@ -3,15 +3,13 @@ import { FastifyInstance } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { migrations } from 'server/util';
|
||||
|
||||
async function prismaPlugin(fastify: FastifyInstance, _, done) {
|
||||
async function prismaPlugin(fastify: FastifyInstance) {
|
||||
process.env.DATABASE_URL = fastify.config.core?.database_url;
|
||||
await migrations();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
fastify.decorate('prisma', prisma);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
export default fastifyPlugin(prismaPlugin, {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { checkPassword } from 'lib/util';
|
||||
|
||||
export default async function rawRoute(this: FastifyInstance, req: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = req.params as { id: string };
|
||||
const { password } = req.query as { password: string };
|
||||
if (id === '') return reply.notFound();
|
||||
|
||||
const file = await this.prisma.file.findFirst({
|
||||
@@ -16,14 +18,20 @@ export default async function rawRoute(this: FastifyInstance, req: FastifyReques
|
||||
if (failed) return reply.notFound();
|
||||
|
||||
if (file.password) {
|
||||
return reply
|
||||
.type('application/json')
|
||||
.code(403)
|
||||
.send({
|
||||
error: "can't view a raw file that has a password",
|
||||
url: `/view/${file.name}`,
|
||||
code: 403,
|
||||
});
|
||||
} else return reply.rawFile(file.name);
|
||||
if (!password)
|
||||
return reply
|
||||
.type('application/json')
|
||||
.code(403)
|
||||
.send({ error: 'password protected', url: `/view/${file.name}`, code: 403 });
|
||||
const success = await checkPassword(password, file.password);
|
||||
|
||||
if (!success)
|
||||
return reply
|
||||
.type('application/json')
|
||||
.code(403)
|
||||
.send({ error: 'incorrect password', url: `/view/${file.name}`, code: 403 });
|
||||
}
|
||||
|
||||
return reply.rawFile(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function uploadsRoute(this: FastifyInstance, req: FastifyRe
|
||||
|
||||
const image = await this.prisma.file.findFirst({
|
||||
where: {
|
||||
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
||||
OR: [{ name: id }, { name: decodeURI(id) }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
|
||||
},
|
||||
});
|
||||
if (!image) return reply.rawFile(id);
|
||||
@@ -28,7 +28,7 @@ export async function uploadsRouteOnResponse(
|
||||
this: FastifyInstance,
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
done: () => void
|
||||
done: () => void,
|
||||
) {
|
||||
if (reply.statusCode === 200) {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function urlsRouteOnResponse(
|
||||
this: FastifyInstance,
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
done: () => void
|
||||
done: () => void,
|
||||
) {
|
||||
if (reply.statusCode === 200) {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function migrations() {
|
||||
} catch (error) {
|
||||
if (error.message.startsWith('P1001')) {
|
||||
logger.error(
|
||||
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`
|
||||
`Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection`,
|
||||
);
|
||||
logger.debug(error);
|
||||
} else {
|
||||
|
||||
@@ -3,18 +3,17 @@ import { spawn } from 'child_process';
|
||||
import ffmpeg from 'ffmpeg-static';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
import type { Config } from 'lib/config/Config';
|
||||
import Logger from 'lib/logger';
|
||||
import { randomChars } from 'lib/util';
|
||||
import { join } from 'path';
|
||||
import { isMainThread, workerData } from 'worker_threads';
|
||||
import datasource from 'lib/datasource';
|
||||
import config from 'lib/config';
|
||||
|
||||
const { videos, config } = workerData as {
|
||||
const { videos } = workerData as {
|
||||
videos: (File & {
|
||||
thumbnail: Thumbnail;
|
||||
})[];
|
||||
config: Config;
|
||||
};
|
||||
|
||||
const logger = Logger.get('worker::thumbnail').child(randomChars(4));
|
||||
@@ -41,6 +40,10 @@ async function loadThumbnail(path) {
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => {
|
||||
if (code !== 0) {
|
||||
const msg = buffers.join('').trim();
|
||||
logger.debug(`cmd: ${ffmpeg} ${args.join(' ')}`);
|
||||
logger.error(`while ${path} child exited with code ${code}: ${msg}`);
|
||||
|
||||
reject(new Error(`child exited with code ${code}`));
|
||||
} else {
|
||||
const buffer = Buffer.allocUnsafe(buffers.reduce((acc, val) => acc + val.length, 0));
|
||||
@@ -112,7 +115,7 @@ async function start() {
|
||||
},
|
||||
});
|
||||
|
||||
await datasource.save(thumb.name, thumbnail);
|
||||
await datasource.save(thumb.name, thumbnail, { type: 'image/jpeg' });
|
||||
|
||||
logger.info(`thumbnail saved - ${thumb.name}`);
|
||||
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
|
||||
|
||||
@@ -59,7 +59,7 @@ async function start() {
|
||||
logger.debug('starting worker');
|
||||
|
||||
const partials = await readdir(config.core.temp_directory).then((files) =>
|
||||
files.filter((x) => x.startsWith(`zipline_partial_${file.identifier}`))
|
||||
files.filter((x) => x.startsWith(`zipline_partial_${file.identifier}`)),
|
||||
);
|
||||
|
||||
const readChunks = partials.map((x) => {
|
||||
@@ -121,7 +121,9 @@ async function start() {
|
||||
await fd.close();
|
||||
} else {
|
||||
logger.debug('writing file to datasource');
|
||||
await datasource.save(file.filename, Buffer.from(fd as Uint8Array));
|
||||
await datasource.save(file.filename, Buffer.from(fd as Uint8Array), {
|
||||
type: file.mimetype ?? 'application/octet-stream',
|
||||
});
|
||||
}
|
||||
|
||||
const final = await prisma.incompleteFile.update({
|
||||
|
||||
Reference in New Issue
Block a user