Compare commits

..

5 Commits

Author SHA1 Message Date
diced 88fdb2fcc1 fix: unecessary stuff 2023-05-29 17:55:39 -07:00
diced e92d78f671 fix: no thumbnailId 2023-05-28 22:20:15 -07:00
dicedtomato bcd2897c4e Merge branch 'trunk' into feature/vid-thumb 2023-05-28 21:38:49 -07:00
diced 24dacb478d feat: thumbnails final 2023-05-28 21:38:27 -07:00
diced 4893f4a09e feat: thumbnails workers 2023-05-23 22:46:46 -07:00
89 changed files with 4562 additions and 4770 deletions
-3
View File
@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: diced
+1 -1
View File
@@ -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.sh
url: https://zipline.diced.tech
about: Maybe take a look a the docs?
Vendored Regular → Executable
View File
+24 -24
View File
@@ -1,5 +1,5 @@
# Use the Prisma binaries image as the first stage
FROM ghcr.io/diced/prisma-binaries:5.1.x as prisma
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
# Use Alpine Linux as the second stage
FROM node:18-alpine3.16 as base
@@ -9,6 +9,14 @@ 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 ./
@@ -18,59 +26,51 @@ 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_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
# Install production dependencies then temporarily save
RUN yarn workspaces focus --production --all
RUN cp -RL node_modules /tmp/node_modules
# Install the dependencies
RUN yarn install --immutable
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=prisma /prisma-engines /prisma-engines
COPY --from=builder /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
# 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/mimes.json ./mimes.json
COPY --from=builder /zipline/next.config.js ./next.config.js
COPY --from=builder /zipline/public ./public
COPY --from=builder /zipline/node_modules ./node_modules
COPY --from=builder /zipline/node_modules/.prisma/client ./node_modules/.prisma/client
COPY --from=builder /zipline/node_modules/@prisma/client ./node_modules/@prisma/client
# Copy Startup Script
COPY docker-entrypoint.sh /zipline
# Make Startup Script Executable
RUN chmod a+x /zipline/docker-entrypoint.sh && 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"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 dicedtomato
Copyright (c) 2022 dicedtomato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+14 -18
View File
@@ -35,9 +35,17 @@ 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>
@@ -68,18 +76,17 @@ 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/).
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`.
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
```shell
git clone https://github.com/diced/zipline
cd zipline
# npm install
yarn install
# npm run build
yarn build
# npm start
yarn start
```
@@ -112,7 +119,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(Xorg/Wayland) and macOS)
# Flameshot (Linux)
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
@@ -127,13 +134,6 @@ 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,7 +166,3 @@ 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
View File
@@ -2,6 +2,4 @@
set -e
unset ZIPLINE_DOCKER_BUILD
node --enable-source-maps dist/index.js
+54 -54
View File
@@ -1,6 +1,6 @@
{
"name": "zipline",
"version": "3.7.7",
"version": "3.7.0",
"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.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",
"@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",
"@tanstack/react-query": "^4.28.0",
"argon2": "^0.31.2",
"cookie": "^0.6.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"argon2": "^0.30.3",
"cookie": "^0.5.0",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"dotenv-expand": "^10.0.0",
"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",
"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",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^14.0.3",
"next": "^13.2.4",
"otplib": "^12.0.1",
"prisma": "^5.1.1",
"prisma": "^4.10.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"qrcode": "^1.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.6",
"recharts": "^2.10.1",
"recharts": "^2.5.0",
"recoil": "^0.7.7",
"remark-gfm": "^4.0.0",
"sharp": "^0.32.6"
"remark-gfm": "^3.0.1",
"sharp": "^0.32.0"
},
"devDependencies": {
"@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",
"@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",
"cross-env": "^7.0.3",
"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",
"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",
"npm-run-all": "^4.1.5",
"prettier": "^3.1.0",
"tsup": "^8.0.0",
"typescript": "^5.2.2"
"prettier": "^2.8.7",
"tsup": "^6.7.0",
"typescript": "^5.0.2"
},
"repository": {
"type": "git",
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "File" ALTER COLUMN "size" SET DATA TYPE BIGINT;
+2 -2
View File
@@ -48,7 +48,7 @@ model File {
originalName String?
mimetype String @default("image/png")
createdAt DateTime @default(now())
size BigInt @default(0)
size Int @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 {
+1 -1
View File
@@ -1498,4 +1498,4 @@ wheat
white
whitesmoke
yellow
yellowgreen
yellowgreen
+1 -1
View File
@@ -1747,4 +1747,4 @@ zigzagsalamander
zonetailedpigeon
zooplankton
zopilote
zorilla
zorilla
+1 -1
View File
@@ -125,7 +125,7 @@ export default function FileModal({
icon: <IconPhotoCancel size='1rem' />,
});
},
},
}
);
};
+10 -26
View File
@@ -280,7 +280,7 @@ export default function Layout({ children, props }) {
component={Link}
href={link}
/>
),
)
)}
</Navbar.Section>
<Navbar.Section>
@@ -349,22 +349,13 @@ export default function Layout({ children, props }) {
<Menu.Target>
<Button
leftIcon={
avatar ? (
<Image src={avatar} height={32} width={32} fit='cover' radius='md' />
) : (
<IconUserCog size='1rem' />
)
avatar ? <Image src={avatar} height={32} radius='md' /> : <IconUserCog size='1rem' />
}
variant='subtle'
color='gray'
compact
size='xl'
p='sm'
styles={{
label: {
overflow: 'unset',
},
}}
>
{user.username}
</Button>
@@ -416,20 +407,16 @@ 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) => (
<>
@@ -442,11 +429,8 @@ 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}
+8 -12
View File
@@ -27,7 +27,7 @@ import PrismCode from './render/PrismCode';
function PlaceholderContent({ text, Icon }) {
return (
<Group sx={(t) => ({ color: t.colors.dark[2], padding: 3, justifyContent: 'center' })}>
<Group sx={(t) => ({ color: t.colors.dark[2] })}>
<Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
@@ -58,27 +58,23 @@ function VideoThumbnailPlaceholder({ file, mediaPreview, ...props }) {
return <Placeholder Icon={IconPlayerPlay} text={`Click to view video (${file.name})`} {...props} />;
return (
<Box sx={{ position: 'relative' }}>
<Box>
<Image
src={file.thumbnail}
sx={{
position: 'absolute',
width: '100%',
height: 'auto',
height: '100%',
objectFit: 'cover',
}}
/>
<Center
sx={{
position: 'absolute',
height: '100%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<Center sx={{ position: 'absolute', width: '100%', height: '100%' }}>
<IconPlayerPlay size={48} />
</Center>
</Box>
// </Placeholder>
);
}
+1 -1
View File
@@ -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, flexDirection: 'column' }}>
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
<IconPhoto size={80} />
<Text size='xl' inline>
+1 -1
View File
@@ -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' : ''}`);
+1 -5
View File
@@ -7,16 +7,12 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';
import FilePagation from './FilePagation';
import PendingFilesModal from './PendingFilesModal';
import { showNonMediaSelector } from 'lib/recoil/settings';
import { useRecoilState } from 'recoil';
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [checked] = useRecoilState(showNonMediaSelector);
const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, {
filter: checked ? 'none' : 'media',
filter: 'media',
favorite: true,
});
+40 -59
View File
@@ -50,12 +50,12 @@ 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 = expireReadToDate(values.expires);
const expiresAt = values.expires === 'never' ? null : expireReadToDate(values.expires);
setOpen(false);
const res = await useFetch('/api/auth/invite', 'POST', {
expiresAt: `date=${expiresAt.toISOString()}`,
expiresAt: expiresAt === null ? null : `date=${expiresAt.toISOString()}`,
count: values.count,
});
@@ -95,6 +95,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
{ value: '3d', label: '3 days' },
{ value: '5d', label: '5 days' },
{ value: '7d', label: '7 days' },
{ value: 'never', label: 'Never' },
]}
/>
@@ -298,65 +299,45 @@ export default function Invites() {
/>
) : (
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
{!ok && !invites.length && (
<>
{[1, 2, 3].map((x) => (
<Skeleton key={x} width='100%' height={100} radius='sm' />
))}
</>
)}
{invites.length && ok ? (
invites.map((invite) => (
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
<Group position='apart'>
<Group position='left'>
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
{invite.id}
</Avatar>
<Stack spacing={0}>
<Title>
{invite.code}
{invite.used && <> (Used)</>}
</Title>
<Tooltip label={new Date(invite.createdAt).toLocaleString()}>
<div>
<MutedText size='sm'>Created {relativeTime(new Date(invite.createdAt))}</MutedText>
</div>
</Tooltip>
<Tooltip label={new Date(invite.expiresAt).toLocaleString()}>
<div>
<MutedText size='sm'>{expireText(invite.expiresAt.toString())}</MutedText>
</div>
</Tooltip>
{invites.length
? invites.map((invite) => (
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
<Group position='apart'>
<Group position='left'>
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
{invite.id}
</Avatar>
<Stack spacing={0}>
<Title>
{invite.code}
{invite.used && <> (Used)</>}
</Title>
<Tooltip label={new Date(invite.createdAt).toLocaleString()}>
<div>
<MutedText size='sm'>
Created {relativeTime(new Date(invite.createdAt))}
</MutedText>
</div>
</Tooltip>
<Tooltip label={new Date(invite.expiresAt).toLocaleString()}>
<div>
<MutedText size='sm'>{expireText(invite.expiresAt.toString())}</MutedText>
</div>
</Tooltip>
</Stack>
</Group>
<Stack>
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
<IconClipboardCopy size='1rem' />
</ActionIcon>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
<IconTrash size='1rem' />
</ActionIcon>
</Stack>
</Group>
<Stack>
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
<IconClipboardCopy size='1rem' />
</ActionIcon>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
<IconTrash size='1rem' />
</ActionIcon>
</Stack>
</Group>
</Card>
))
) : (
<>
<div></div>
<Group>
<div>
<IconTag size={48} />
</div>
<div>
<Title>Nothing here</Title>
<MutedText size='md'>Create some invites and they will show up here</MutedText>
</div>
</Group>
<div></div>
</>
)}
</Card>
))
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
</SimpleGrid>
)}
</>
+1 -1
View File
@@ -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;if [ ! -s /tmp/ss.png ]; then\n exit 1\nfi
flameshot gui -r > /tmp/ss.png;
${curl.join(' ')}${values.noJSON ? '' : " | jq -r '.files[0]'"} | tr -d '\\n' | ${
values.wlCompatibility ? 'wl-copy' : 'xsel -ib'
};
+1 -1
View File
@@ -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';
+4 -3
View File
@@ -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,7 +367,8 @@ 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.sh/docs/guides/variables'>the docs</AnchorNext> for variables
<AnchorNext href='https://zipline.diced.tech/docs/guides/variables'>the docs</AnchorNext> for
variables
</MutedText>
<TextInput
@@ -487,7 +488,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'>
+28 -68
View File
@@ -1,26 +1,19 @@
import { Anchor, Button, Collapse, Group, Progress, Stack, Text, Title } from '@mantine/core';
import { Button, Collapse, Group, Progress, Stack, Title } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { hideNotification, showNotification, updateNotification } from '@mantine/notifications';
import {
IconClipboardCopy,
IconFileImport,
IconFileTime,
IconFileUpload,
IconFileX,
} from '@tabler/icons-react';
import { showNotification, updateNotification } from '@mantine/notifications';
import { IconFileImport, IconFileTime, IconFileUpload, IconFileX } from '@tabler/icons-react';
import Dropzone from 'components/dropzone/Dropzone';
import FileDropzone from 'components/dropzone/DropzoneFile';
import MutedText from 'components/MutedText';
import { invalidateFiles } from 'lib/queries/files';
import { userSelector } from 'lib/recoil/user';
import { expireReadToDate, randomChars } from 'lib/utils/client';
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import showFilesModal from './showFilesModal';
import useUploadOptions from './useUploadOptions';
import { useRouter } from 'next/router';
import AnchorNext from 'components/AnchorNext';
export default function File({ chunks: chunks_config }) {
const router = useRouter();
@@ -35,29 +28,23 @@ export default function File({ chunks: chunks_config }) {
const [options, setOpened, OptionsModal] = useUploadOptions();
const beforeUnload = useCallback(
(e: BeforeUnloadEvent) => {
if (loading) {
e.preventDefault();
e.returnValue = "Are you sure you want to leave? Your upload(s) won't be saved.";
return e.returnValue;
}
},
[loading],
);
const beforeUnload = (e: BeforeUnloadEvent) => {
if (loading) {
e.preventDefault();
e.returnValue = "Are you sure you want to leave? Your upload(s) won't be saved.";
return e.returnValue;
}
};
const beforeRouteChange = useCallback(
(url: string) => {
if (loading) {
const confirmed = confirm("Are you sure you want to leave? Your upload(s) won't be saved.");
if (!confirmed) {
router.events.emit('routeChangeComplete', url);
throw 'Route change aborted';
}
const beforeRouteChange = (url: string) => {
if (loading) {
const confirmed = confirm("Are you sure you want to leave? Your upload(s) won't be saved.");
if (!confirmed) {
router.events.emit('routeChangeComplete', url);
throw 'Route change aborted';
}
},
[loading],
);
}
};
useEffect(() => {
const listener = (e: ClipboardEvent) => {
@@ -75,24 +62,16 @@ export default function File({ chunks: chunks_config }) {
};
document.addEventListener('paste', listener);
window.addEventListener('beforeunload', beforeUnload, true);
window.addEventListener('beforeunload', beforeUnload);
router.events.on('routeChangeStart', beforeRouteChange);
return () => {
window.removeEventListener('beforeunload', beforeUnload, true);
window.removeEventListener('beforeunload', beforeUnload);
router.events.off('routeChangeStart', beforeRouteChange);
document.removeEventListener('paste', listener);
};
}, [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);
@@ -146,34 +125,15 @@ export default function File({ chunks: chunks_config }) {
updateNotification({
id: 'upload-chunked',
title: 'Finalizing partial upload',
message: (
<Text>
The upload has been offloaded, and will complete in the background.
<br />
<Anchor
component='span'
onClick={() => {
hideNotification('upload-chunked');
clipboard.copy(json.files[0]);
showNotification({
title: 'Copied to clipboard',
message: <AnchorNext href={json.files[0]}>{json.files[0]}</AnchorNext>,
icon: <IconClipboardCopy size='1rem' />,
});
}}
>
Click here to copy the URL while it&lsquo;s being processed.
</Anchor>
</Text>
),
message:
'The upload has been offloaded, and will complete in the background. You can see processing files in the files tab.',
icon: <IconFileTime size='1rem' />,
color: 'green',
autoClose: false,
autoClose: true,
});
invalidateFiles();
setFiles([]);
setProgress(100);
setLoading(false);
setTimeout(() => setProgress(0), 1000);
}
@@ -191,7 +151,7 @@ export default function File({ chunks: chunks_config }) {
ready = false;
}
},
false,
false
);
req.open('POST', '/api/upload');
@@ -230,10 +190,10 @@ export default function File({ chunks: chunks_config }) {
for (let i = 0; i !== files.length; ++i) {
const file = files[i];
if (chunks_config.enabled && file.size >= chunks_config.max_size) {
if (file.size >= chunks_config.max_size) {
toChunkFiles.push(file);
} else {
body.append('file', files[i], encodeURIComponent(files[i].name));
body.append('file', files[i]);
}
}
@@ -307,7 +267,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',
-6
View File
@@ -57,7 +57,6 @@ export interface ConfigUploader {
format_date: string;
default_expiration: string;
assume_mimetypes: boolean;
random_words_separator: string;
}
export interface ConfigUrls {
@@ -124,8 +123,6 @@ export interface ConfigFeatures {
default_avatar: string;
robots_txt: string;
thumbnails: boolean;
}
export interface ConfigOAuth {
@@ -136,12 +133,9 @@ 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 {
-6
View File
@@ -98,7 +98,6 @@ 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'),
@@ -147,12 +146,9 @@ 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'),
@@ -167,8 +163,6 @@ export default function readConfig() {
map('FEATURES_ROBOTS_TXT', 'boolean', 'features.robots_txt'),
map('FEATURES_THUMBNAILS', 'boolean', 'features.thumbnails'),
map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'),
map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'),
map('CHUNKS_ENABLED', 'boolean', 'chunks.enabled'),
+3 -9
View File
@@ -97,7 +97,6 @@ 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',
@@ -141,11 +140,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.sh/' },
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
]),
})
.default({
@@ -156,7 +155,7 @@ const validator = s.object({
external_links: [
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
{ label: 'Documentation', link: 'https://zipline.diced.sh/' },
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
],
}),
discord: s
@@ -177,12 +176,9 @@ 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
@@ -195,7 +191,6 @@ const validator = s.object({
headless: s.boolean.default(false),
default_avatar: s.string.nullable.default(null),
robots_txt: s.boolean.default(false),
thumbnails: s.boolean.default(false),
})
.default({
invites: false,
@@ -206,7 +201,6 @@ const validator = s.object({
headless: false,
default_avatar: null,
robots_txt: false,
thumbnails: false,
}),
chunks: s
.object({
+7 -4
View File
@@ -49,10 +49,13 @@ export class S3 extends Datasource {
});
}
public async size(file: string): Promise<number> {
const stat = await this.s3.statObject(this.config.bucket, file);
return stat.size;
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 fullSize(): Promise<number> {
+6 -7
View File
@@ -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 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');
if (!config.discord.upload) return;
if (!config.discord.url && !config.discord.upload.url) return;
logger.debug(`discord config:\n${JSON.stringify(config.discord)}`);
const parsed = parseContent(config.discord.upload, {
file,
user,
@@ -97,9 +97,8 @@ 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 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');
if (!config.discord.shorten) return;
if (!config.discord.url && !config.discord.shorten.url) return;
const parsed = parseContent(config.discord.shorten, {
url,
+9 -24
View File
@@ -1,41 +1,26 @@
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 | 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());
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');
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;
}
return {
adjectives,
animals,
};
}
function randomWord(words: string[]) {
return words[Math.floor(Math.random() * words.length)];
}
export default async function gfycat(): Promise<string | null> {
export default async function gfycat() {
const words = await importWords();
if (!words) return null;
return `${randomWord(words.adjectives)}${config.uploader.random_words_separator}${randomWord(
words.adjectives,
)}${config.uploader.random_words_separator}${randomWord(words.animals)}`;
return `${randomWord(words.adjectives)}${randomWord(words.adjectives)}${randomWord(words.animals)}`;
}
+1 -1
View File
@@ -19,7 +19,7 @@ export default async function formatFileName(nameFormat: NameFormat, originalNam
return name;
case 'gfycat':
return gfycat() ?? random();
return gfycat();
default:
return random();
}
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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;
+4 -12
View File
@@ -1,5 +1,5 @@
import config from 'lib/config';
import { isNotNullOrUndefined } from 'lib/util';
import { notNull } from 'lib/util';
import { GetServerSideProps } from 'next';
export type OauthProvider = {
@@ -19,7 +19,6 @@ export type ServerSideProps = {
bypass_local_login: boolean;
chunks_size: number;
max_size: number;
chunks_enabled: boolean;
totp_enabled: boolean;
exif_enabled: boolean;
fileId?: string;
@@ -27,15 +26,9 @@ export type ServerSideProps = {
};
export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ctx) => {
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 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 oauth_providers: OauthProvider[] = [];
@@ -72,7 +65,6 @@ 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,
+2 -2
View File
@@ -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');
+8 -8
View File
@@ -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
);
}
+5 -5
View File
@@ -16,9 +16,9 @@ export const github_auth = {
};
export const discord_auth = {
oauth_url: (clientId: string, origin: string, state?: string, redirect_uri?: string) =>
oauth_url: (clientId: string, origin: string, state?: string) =>
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirect_uri || `${origin}/api/auth/oauth/discord`,
`${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, redirect_uri?: string) =>
oauth_url: (clientId: string, origin: string, state?: string) =>
`https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirect_uri || `${origin}/api/auth/oauth/google`,
`${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;
+5 -5
View File
@@ -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']);
},
},
}
);
}
+16 -6
View File
@@ -17,17 +17,27 @@ 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[]>,
);
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(),
}))
);
});
};
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>,
);
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(),
}));
});
};
+1 -1
View File
@@ -27,6 +27,6 @@ export const useStats = (amount = 2) => {
},
{
staleTime: 1000 * 60 * 5, // 5 minutes
},
}
);
};
+1 -1
View File
@@ -36,6 +36,6 @@ export function useURLDelete() {
?.filter((u) => u.id !== variables);
queryClient.setQueryData(['urls'], dataWithoutDeleted);
},
},
}
);
}
+3 -5
View File
@@ -15,12 +15,10 @@ export const useVersion = () => {
return useQuery<VersionResponse>(
['version'],
async () => {
return fetch('/api/version').then((res) => (res.ok ? res.json() : Promise.reject('')));
return fetch('/api/version').then((res) => res.json());
},
{
refetchInterval: false,
refetchOnMount: false,
retry: false,
},
staleTime: Infinity,
}
);
};
+6 -6
View File
@@ -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.sh', <IconHelp />),
actionLink('Help', 'Documentation', 'View the documentation', 'https://zipline.diced.tech', <IconHelp />),
// the list of actions here is very incomplete, and will be expanded in the future
];
+2 -2
View File
@@ -120,6 +120,6 @@ export async function getBase64URLFromURL(url: string) {
return `data:${res.headers.get('content-type')};base64,${base64}`;
}
export function isNotNullOrUndefined(value: unknown) {
return value !== null && value !== undefined;
export function notNull(a: unknown, b: unknown) {
return a !== null && b !== null;
}
+1 -1
View File
@@ -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]
);
}
+37 -44
View File
@@ -1,10 +1,10 @@
import { File } from '@prisma/client';
import { ExifTool, Tags } from 'exiftool-vendored';
import { createWriteStream } from 'fs';
import { readFile, rm } from 'fs/promises';
import { ExifTool, Tags } from 'exiftool-vendored';
import datasource from 'lib/datasource';
import Logger from 'lib/logger';
import { join } from 'path';
import { readFile, unlink } from 'fs/promises';
const logger = Logger.get('exif');
@@ -43,54 +43,47 @@ export async function removeGPSData(image: File): Promise<void> {
await new Promise((resolve) => writeStream.on('finish', resolve));
logger.debug(`removing GPS data from ${file}`);
try {
await exiftool.write(file, {
GPSVersionID: null,
GPSAltitude: null,
GPSAltitudeRef: null,
GPSAreaInformation: null,
GPSDateStamp: null,
GPSDateTime: null,
GPSDestBearing: null,
GPSDestBearingRef: null,
GPSDestDistance: null,
GPSDestLatitude: null,
GPSDestLatitudeRef: null,
GPSDestLongitude: null,
GPSDestLongitudeRef: null,
GPSDifferential: null,
GPSDOP: null,
GPSHPositioningError: null,
GPSImgDirection: null,
GPSImgDirectionRef: null,
GPSLatitude: null,
GPSLatitudeRef: null,
GPSLongitude: null,
GPSLongitudeRef: null,
GPSMapDatum: null,
GPSPosition: null,
GPSProcessingMethod: null,
GPSSatellites: null,
GPSSpeed: null,
GPSSpeedRef: null,
GPSStatus: null,
GPSTimeStamp: null,
GPSTrack: null,
GPSTrackRef: null,
});
} catch (e) {
logger.debug(`removing temp file: ${file}`);
await rm(file);
return;
}
await exiftool.write(file, {
GPSVersionID: null,
GPSAltitude: null,
GPSAltitudeRef: null,
GPSAreaInformation: null,
GPSDateStamp: null,
GPSDateTime: null,
GPSDestBearing: null,
GPSDestBearingRef: null,
GPSDestDistance: null,
GPSDestLatitude: null,
GPSDestLatitudeRef: null,
GPSDestLongitude: null,
GPSDestLongitudeRef: null,
GPSDifferential: null,
GPSDOP: null,
GPSHPositioningError: null,
GPSImgDirection: null,
GPSImgDirectionRef: null,
GPSLatitude: null,
GPSLatitudeRef: null,
GPSLongitude: null,
GPSLongitudeRef: null,
GPSMapDatum: null,
GPSPosition: null,
GPSProcessingMethod: null,
GPSSatellites: null,
GPSSpeed: null,
GPSSpeedRef: null,
GPSStatus: null,
GPSTimeStamp: null,
GPSTrack: null,
GPSTrackRef: null,
});
logger.debug(`reading file to upload to datasource: ${file} -> ${image.name}`);
const buffer = await readFile(file);
await datasource.save(image.name, buffer);
logger.debug(`removing temp file: ${file}`);
await rm(file);
await unlink(file);
await exiftool.end(true);
+1 -5
View File
@@ -1,5 +1,4 @@
import type { File, User, Url } from '@prisma/client';
import { bytesToHuman } from './bytes';
export type ParseValue = {
file?: File;
@@ -33,13 +32,12 @@ 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;
@@ -127,8 +125,6 @@ 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 -1
View File
@@ -69,7 +69,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
logger.info(
`Created user ${newUser.username} (${newUser.id}) ${
code ? `from invite code ${code}` : 'via registration'
}`,
}`
);
return res.json({ success: true });
+1 -1
View File
@@ -37,7 +37,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);
+1 -1
View File
@@ -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 });
}
+4 -16
View File
@@ -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, isNotNullOrUndefined } from 'lib/util';
import { getBase64URLFromURL, notNull } from 'lib/util';
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,10 +12,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
error: 'oauth registration is disabled',
};
if (
!isNotNullOrUndefined(config.oauth.discord_client_id) &&
!isNotNullOrUndefined(config.oauth.discord_client_secret)
) {
if (!notNull(config.oauth.discord_client_id, config.oauth.discord_client_secret)) {
logger.error('Discord OAuth is not configured');
return {
@@ -29,8 +26,7 @@ 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,
config.oauth.discord_redirect_uri,
state
),
};
@@ -39,9 +35,7 @@ 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.oauth.discord_redirect_uri ||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/discord`,
scope: 'identify',
});
@@ -73,12 +67,6 @@ 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,
+2 -5
View File
@@ -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, isNotNullOrUndefined } from 'lib/util';
import { getBase64URLFromURL, notNull } from 'lib/util';
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,10 +12,7 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
error: 'oauth registration is disabled',
};
if (
!isNotNullOrUndefined(config.oauth.github_client_id) &&
!isNotNullOrUndefined(config.oauth.github_client_secret)
) {
if (!notNull(config.oauth.github_client_id, config.oauth.github_client_secret)) {
logger.error('GitHub OAuth is not configured');
return {
error_code: 401,
+4 -10
View File
@@ -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, isNotNullOrUndefined } from 'lib/util';
import { getBase64URLFromURL, notNull } from 'lib/util';
async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
@@ -12,10 +12,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
error: 'oauth registration is disabled',
};
if (
!isNotNullOrUndefined(config.oauth.google_client_id) &&
!isNotNullOrUndefined(config.oauth.google_client_secret)
) {
if (!notNull(config.oauth.google_client_id, config.oauth.google_client_secret)) {
logger.error('Google OAuth is not configured');
return {
error_code: 401,
@@ -28,8 +25,7 @@ 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,
config.oauth.google_redirect_uri,
state
),
};
@@ -37,9 +33,7 @@ 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.oauth.google_redirect_uri ||
`${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/google`,
grant_type: 'authorization_code',
});
+1 -1
View File
@@ -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') {
+17 -57
View File
@@ -80,16 +80,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
// 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 ', '')
@@ -111,7 +101,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
start,
end,
total,
})}`,
})}`
);
const tempFile = join(zconfig.core.temp_directory, `zipline_partial_${identifier}_${start}_${end}`);
@@ -119,38 +109,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
await writeFile(tempFile, req.files[0].buffer);
if (lastchunk) {
const fileName = await formatFileName(format, filename);
const ext = filename.split('.').length === 1 ? '' : filename.split('.').pop();
const file = await prisma.file.create({
data: {
name: `${fileName}${ext ? '.' : ''}${ext}`,
mimetype: req.headers.uploadtext ? 'text/plain' : mimetype,
userId: user.id,
originalName: req.headers['original-name'] ? filename ?? null : null,
},
});
let domain;
if (req.headers['override-domain']) {
domain = `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers['override-domain']}`;
} else if (user.domains.length) {
domain = user.domains[Math.floor(Math.random() * user.domains.length)];
} else {
domain = `${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`;
}
const responseUrl = `${domain}${
zconfig.uploader.route === '/' ? '/' : zconfig.uploader.route + '/'
}${encodeURI(file.name)}`;
new Worker('./dist/worker/upload.js', {
workerData: {
user,
file: {
id: file.id,
filename: file.name,
mimetype: file.mimetype,
filename,
mimetype,
identifier,
lastchunk,
totalBytes: total,
@@ -158,6 +122,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
response: {
expiresAt: expiry,
format,
imageCompressionPercent,
fileMaxViews,
},
headers: req.headers,
@@ -166,7 +131,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
return res.json({
pending: true,
files: [responseUrl],
});
}
@@ -203,32 +167,29 @@ 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 decodedName = decodeURI(file.originalname);
const ext = decodedName.split('.').length === 1 ? '' : decodedName.split('.').pop();
const ext = file.originalname.split('.').length === 1 ? '' : file.originalname.split('.').pop();
if (zconfig.uploader.disabled_extensions.includes(ext))
return res.badRequest(`file[${i}]: disabled extension recieved: ${ext}`);
const fileName = await formatFileName(format, decodedName);
let fileName = await formatFileName(format, file.originalname);
if (format === 'name' || req.headers['x-zipline-filename']) {
const exist = (req.headers['x-zipline-filename'] as string) || decodedName;
if (req.headers['x-zipline-filename']) {
fileName = req.headers['x-zipline-filename'] as string;
const existing = await prisma.file.findFirst({
where: {
name: exist,
name: fileName,
},
});
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${decodedName}'`);
if (existing) return res.badRequest(`file[${i}]: filename already exists: '${fileName}'`);
}
let password = null;
@@ -239,7 +200,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(decodedName).ext.replace('.', '');
const ext = parse(file.originalname).ext.replace('.', '');
const mime = await guess(ext);
if (!mime) response.assumed_mimetype = false;
@@ -260,19 +221,18 @@ async function handler(req: NextApiReq, res: NextApiRes) {
password,
expiresAt: expiry,
maxViews: fileMaxViews,
originalName: req.headers['original-name'] ? decodedName ?? null : null,
originalName: req.headers['original-name'] ? file.originalname ?? null : null,
size: file.size,
},
});
if (typeof req.headers.zws !== 'undefined' && (req.headers.zws as string).toLowerCase().match('true'))
invis = await createInvisImage(zconfig.uploader.length, fileUpload.id);
if (req.headers.zws) invis = await createInvisImage(zconfig.uploader.length, fileUpload.id);
if (compressionUsed) {
const buffer = await sharp(file.buffer).jpeg({ quality: imageCompressionPercent }).toBuffer();
await datasource.save(fileUpload.name, buffer);
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);
@@ -299,7 +259,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
user,
fileUpload,
`${domain}/r/${invis ? invis.invis : encodeURI(fileUpload.name)}`,
responseUrl,
responseUrl
);
}
+8 -26
View File
@@ -3,8 +3,6 @@ import Logger from 'lib/logger';
import prisma from 'lib/prisma';
import { hashPassword } from 'lib/util';
import { jsonUserReplacer } from 'lib/utils/client';
import { formatRootUrl } from 'lib/utils/urls';
import zconfig from 'lib/config';
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
const logger = Logger.get('user');
@@ -17,11 +15,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
id: Number(id),
},
include: {
files: {
include: {
thumbnail: true,
},
},
files: true,
Folder: true,
},
});
@@ -36,7 +30,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 +55,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
where: {
userId: target.id,
},
}),
})
);
}
Promise.all(promises).then((promised) => {
@@ -71,10 +65,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 +171,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;
@@ -185,21 +179,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
} else {
delete target.password;
if (user.superAdmin && target.superAdmin) {
if (user.superAdmin && target.superAdmin) delete target.files;
if (user.administrator && !user.superAdmin && (target.administrator || target.superAdmin))
delete target.files;
return res.json(target);
}
if (user.administrator && !user.superAdmin && (target.administrator || target.superAdmin)) {
delete target.files;
return res.json(target);
}
for (const file of target.files) {
(file as unknown as { url: string }).url = formatRootUrl(zconfig.uploader.route, file.name);
if (file.thumbnail) {
(file.thumbnail as unknown as string) = formatRootUrl('/r', file.thumbnail.name);
}
}
return res.json(target);
}
+10 -11
View File
@@ -3,19 +3,18 @@ import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
async function handler(req: NextApiReq, res: NextApiRes) {
const { code, username } = req.body as { code?: string; username?: string };
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');
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');
if (!req.body?.code) return res.badRequest('no code');
if (!req.body?.username) return res.badRequest('no username');
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 { 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');
const user = await prisma.user.findFirst({
where: { username },
+1 -1
View File
@@ -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 {
+2 -9
View File
@@ -14,14 +14,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
where: {
userId: user.id,
},
include: {
thumbnail: true,
},
});
for (let i = 0; i !== files.length; ++i) {
await datasource.delete(files[i].name);
if (files[i].thumbnail?.name) await datasource.delete(files[i].thumbnail.name);
}
const { count } = await prisma.file.deleteMany({
@@ -49,7 +45,6 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
id: true,
},
},
thumbnail: true,
},
});
@@ -68,15 +63,13 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
id: true,
},
},
thumbnail: true,
},
});
await datasource.delete(file.name);
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 +132,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
expiresAt: Date;
maxViews: number;
views: number;
size: bigint;
size: number;
originalName: string;
thumbnail?: { name: string };
}[] = await prisma.file.findMany({
+6 -6
View File
@@ -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
);
}
}
+2 -2
View File
@@ -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
);
}
}
+5 -5
View File
@@ -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}`
),
});
}
+1 -1
View File
@@ -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');
+2 -2
View File
@@ -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: bigint;
size: number;
password: string | boolean;
thumbnail?: { name: string };
}[] = await prisma.file.findMany({
+1 -1
View File
@@ -38,7 +38,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);
+1 -1
View File
@@ -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.sh/api/version?c=' + pkg.version);
const re = await fetch('https://zipline.diced.tech/api/version?c=' + pkg.version);
const json = await re.json();
let updateToType = 'stable';
+13 -1
View File
@@ -6,13 +6,14 @@ 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 = undefined, title, user_registration }) {
export default function Register({ code, title, user_registration }) {
const [active, setActive] = useState(0);
const [username, setUsername] = useState('');
const [usernameError, setUsernameError] = useState('');
@@ -195,9 +196,20 @@ 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,
},
};
+1 -3
View File
@@ -17,9 +17,7 @@ export default function UploadPage(props) {
<title>{title}</title>
</Head>
<Layout props={props}>
<File
chunks={{ chunks_size: props.chunks_size, max_size: props.max_size, enabled: props.chunks_enabled }}
/>
<File chunks={{ chunks_size: props.chunks_size, max_size: props.max_size }} />
</Layout>
</>
);
+1 -3
View File
@@ -12,7 +12,6 @@ type LimitedFolder = {
createdAt: Date | string;
mimetype: string;
views: number;
size: bigint;
}[];
user: {
username: string;
@@ -84,7 +83,6 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
views: true,
createdAt: true,
password: true,
size: true,
},
},
user: {
@@ -103,7 +101,7 @@ 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
+22 -70
View File
@@ -1,5 +1,5 @@
import { Box, Button, Modal, PasswordInput, Title } from '@mantine/core';
import type { File, Thumbnail } from '@prisma/client';
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
import type { File } from '@prisma/client';
import AnchorNext from 'components/AnchorNext';
import exts from 'lib/exts';
import prisma from 'lib/prisma';
@@ -10,35 +10,27 @@ import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import zconfig from 'lib/config';
export default function EmbeddedFile({
file,
user,
pass,
prismRender,
host,
compress,
}: {
file: File & { imageProps?: HTMLImageElement; thumbnail: Thumbnail };
file: File & { imageProps?: HTMLImageElement };
user: UserExtended;
pass: boolean;
prismRender: boolean;
host: string;
compress?: boolean;
}) {
const dataURL = (route: string, pass?: string) =>
`${route}/${encodeURIComponent(file.name)}?compress=${compress ?? false}${
pass ? `&password=${encodeURIComponent(pass)}` : ''
}`;
const dataURL = (route: string) => `${route}/${encodeURI(file.name)}?compress=${compress ?? false}`;
const router = useRouter();
const [opened, setOpened] = useState(pass || !!file.password);
const [opened, setOpened] = useState(pass);
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);
@@ -50,22 +42,19 @@ 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),
this.naturalWidth * Math.min(innerHeight / this.naturalHeight, innerWidth / this.naturalWidth)
);
else imageEl.width = this.naturalWidth;
});
@@ -110,37 +99,26 @@ export default function EmbeddedFile({
{file.mimetype.startsWith('image') && (
<>
<meta property='og:type' content='image' />
<meta property='og:image' itemProp='image' content={`${host}/r/${file.name}`} />
<meta property='og:url' content={`${host}/r/${file.name}`} />
<meta property='og:image' itemProp='image' content={`/r/${file.name}`} />
<meta property='og:url' content={`/r/${file.name}`} />
<meta property='og:image:width' content={file.imageProps?.naturalWidth.toString()} />
<meta property='og:image:height' content={file.imageProps?.naturalHeight.toString()} />
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:image' content={`${host}/r/${file.name}`} />
<meta property='twitter:title' content={file.name} />
</>
)}
{file.mimetype.startsWith('video') && (
<>
<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:stream' content={`/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} />
{file.thumbnail && (
<>
<meta name='twitter:image' content={`${host}/r/${file.thumbnail.name}`} />
<meta property='og:image' content={`${host}/r/${file.thumbnail.name}`} />
</>
)}
<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:url' content={`/r/${file.name}`} />
<meta property='og:video' content={`/r/${file.name}`} />
<meta property='og:video:url' content={`/r/${file.name}`} />
<meta property='og:video:secure_url' content={`/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' />
@@ -149,35 +127,33 @@ export default function EmbeddedFile({
{file.mimetype.startsWith('audio') && (
<>
<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:stream' content={`/r/${file.name}`} />
<meta name='twitter:player:stream:content_type' content={file.mimetype} />
<meta name='twitter:title' content={file.name} />
<meta name='twitter:player:width' content='720' />
<meta name='twitter:player:height' content='480' />
<meta property='og:type' content='music.song' />
<meta property='og:url' content={`${host}/r/${file.name}`} />
<meta property='og:audio' content={`${host}/r/${file.name}`} />
<meta property='og:audio:secure_url' content={`${host}/r/${file.name}`} />
<meta property='og:url' content={`/r/${file.name}`} />
<meta property='og:audio' content={`/r/${file.name}`} />
<meta property='og:audio:secure_url' content={`/r/${file.name}`} />
<meta property='og:audio:type' content={file.mimetype} />
</>
)}
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
<meta property='og:url' content={`${host}/r/${file.name}`} />
<meta property='og:url' content={`/r/${file.name}`} />
)}
<title>{file.name}</title>
</Head>
<Modal
opened={opened}
onClose={() => setOpened(false)}
title={<Title order={3}>Password Protected</Title>}
title='Password Protected'
centered={true}
withCloseButton={false}
withCloseButton={true}
closeOnEscape={false}
closeOnClickOutside={false}
>
<PasswordInput
label='Password'
placeholder='Password'
error={error}
value={password}
@@ -210,7 +186,7 @@ export default function EmbeddedFile({
{!file.mimetype.startsWith('video') &&
!file.mimetype.startsWith('image') &&
!file.mimetype.startsWith('audio') && (
<AnchorNext component={Link} href={dataURL('/r', downloadWPass ? password : undefined)}>
<AnchorNext component={Link} href={dataURL('/r')}>
Can&#39;t preview this file. Click here to download it.
</AnchorNext>
)}
@@ -226,30 +202,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
where: {
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
},
include: {
thumbnail: true,
},
});
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 (
JSON.parse(context.req.headers['cf-visitor'] as string).scheme === 'https' ||
proto === 'https' ||
zconfig.core.return_https
)
host = `https://${host}`;
else host = `http://${host}`;
} catch (e) {
if (proto === 'https' || zconfig.core.return_https) host = `https://${host}`;
else host = `http://${host}`;
}
const user = await prisma.user.findFirst({
where: {
id: file.userId,
@@ -280,7 +235,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
user,
pass,
prismRender: true,
host,
},
};
}
@@ -298,7 +252,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
props: {
file,
user,
host,
},
};
}
@@ -311,7 +264,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
file,
user,
pass: file.password ? true : false,
host,
compress,
},
};
+1 -3
View File
@@ -11,9 +11,7 @@ async function main() {
process.exit(0);
}
const files = (await readdir(temp)).filter(
(x) => x.startsWith('zipline_partial_') || x.startsWith('zipline_thumb_'),
);
const files = (await readdir(temp)).filter((x) => x.startsWith('zipline_partial_'));
if (files.length === 0) {
console.log('No partial files found, exiting..');
process.exit(0);
-3
View File
@@ -47,9 +47,6 @@ async function main() {
},
},
});
await prisma.$disconnect();
console.log(`Deleted ${count} files from the database.`);
for (let i = 0; i !== toDelete.length; ++i) {
+1 -8
View File
@@ -1,12 +1,10 @@
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];
@@ -27,16 +25,13 @@ 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({
name: files[i],
mimetype: mime,
userId,
size,
});
console.log(`Imported ${files[i]} (${bytesToHuman(size)}) (${mime} mimetype) to user ${userId}`);
console.log(`Imported ${files[i]} (${mime} mimetype) to user ${userId}`);
}
process.env.DATABASE_URL = config.core.database_url;
@@ -57,8 +52,6 @@ async function main() {
await datasource.save(file, await readFile(join(directory, file)));
}
console.log(`Finished copying files to ${config.datasource.type} storage.`);
process.exit(0);
}
main();
+1 -7
View File
@@ -1,7 +1,6 @@
import { PrismaClient } from '@prisma/client';
import config from 'lib/config';
import { migrations } from 'server/util';
import { inspect } from 'util';
async function main() {
const extras = (process.argv[2] ?? '').split(',');
@@ -14,7 +13,6 @@ async function main() {
const select = {
username: true,
administrator: true,
superAdmin: true,
id: true,
};
for (let i = 0; i !== extras.length; ++i) {
@@ -32,11 +30,7 @@ async function main() {
select,
});
await prisma.$disconnect();
console.log(inspect(users, false, 4, true));
process.exit(0);
console.log(JSON.stringify(users, null, 2));
}
main();
+1 -4
View File
@@ -60,14 +60,11 @@ async function main() {
}
}
await prisma.$disconnect();
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.');
process.exit(0);
}
-4
View File
@@ -66,15 +66,11 @@ async function main() {
data,
});
await prisma.$disconnect();
if (args[1] === 'password') {
parsed = '***';
}
console.log(`Updated user ${user.id} with ${args[1]} = ${parsed}`);
process.exit(0);
}
main();
+1 -1
View File
@@ -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="${encodeURI(file.originalName || file.name)}"`);
this.header('Content-Disposition', `inline; filename="${file.originalName || file.name}"`);
return this.send(data);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { FastifyInstance, FastifyReply } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
function postUrlDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('postUrl', postUrl.bind(fastify));
fastify.decorateReply('postUrl', postUrl);
done();
async function postUrl(this: FastifyReply, url: Url) {
+3 -3
View File
@@ -11,7 +11,7 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
done();
async function rawFile(this: FastifyReply, id: string) {
const { download, compress = 'false' } = this.request.query as { download?: string; compress?: string };
const { download, compress } = this.request.query as { download?: string; compress?: boolean };
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?.match(/^true$/i) &&
compress &&
!this.request.headers['X-Zipline-NoCompress'] &&
!!this.request.headers['accept-encoding']
)
if (size > this.server.config.core.compression.threshold && mimetype.match(/^(image|video|text)/))
if (size > this.server.config.core.compression.threshold)
return this.send(useCompress.call(this, data));
this.header('Content-Length', size);
return this.send(data);
+10 -39
View File
@@ -1,12 +1,11 @@
import config from 'lib/config';
import datasource from 'lib/datasource';
import Logger from 'lib/logger';
import { getStats } from 'server/util';
import { version } from '../../package.json';
import { getStats } from 'server/util';
import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify';
import { createReadStream, existsSync, readFileSync } from 'fs';
import { Worker } from 'worker_threads';
import dbFileDecorator from './decorators/dbFile';
import notFound from './decorators/notFound';
import postFileDecorator from './decorators/postFile';
@@ -22,18 +21,13 @@ import prismaPlugin from './plugins/prisma';
import rawRoute from './routes/raw';
import uploadsRoute, { uploadsRouteOnResponse } from './routes/uploads';
import urlsRoute, { urlsRouteOnResponse } from './routes/urls';
import { Worker } from 'worker_threads';
const dev = process.env.NODE_ENV === 'development';
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));
@@ -93,7 +87,7 @@ async function start() {
url: req.url,
headers: req.headers,
body: req.headers['content-type']?.startsWith('application/json') ? req.body : undefined,
}),
})
);
}
@@ -185,17 +179,16 @@ Disallow: ${config.urls.route}
.info(
`started ${dev ? 'development' : 'production'} zipline@${version} server${
config.features.headless ? ' (headless)' : ''
}`,
}`
);
await clearInvites.bind(server)();
await stats.bind(server)();
if (config.features.thumbnails) await thumbs.bind(server)();
await thumbs.bind(server)();
setInterval(() => clearInvites.bind(server)(), config.core.invites_interval * 1000);
setInterval(() => stats.bind(server)(), config.core.stats_interval * 1000);
if (config.features.thumbnails)
setInterval(() => thumbs.bind(server)(), config.core.thumbnails_interval * 1000);
setInterval(() => thumbs.bind(server)(), config.core.thumbnails_interval * 1000);
}
async function stats(this: FastifyInstance) {
@@ -235,38 +228,16 @@ async function thumbs(this: FastifyInstance) {
},
thumbnail: null,
},
include: {
thumbnail: true,
},
});
// avoids reaching prisma connection limit
const MAX_THUMB_THREADS = 4;
// make all the files fit into 4 arrays
const chunks = [];
for (let i = 0; i !== MAX_THUMB_THREADS; ++i) {
chunks.push([]);
for (let j = i; j < videoFiles.length; j += MAX_THUMB_THREADS) {
chunks[i].push(videoFiles[j]);
}
}
logger.child('thumbnail').debug(`starting ${chunks.length} thumbnail threads`);
for (let i = 0; i !== chunks.length; ++i) {
const chunk = chunks[i];
if (chunk.length === 0) continue;
logger.child('thumbnail').debug(`starting thumbnail generation for ${chunk.length} videos`);
logger.child('thumb').debug(`found ${videoFiles.length} videos without thumbnails`);
for (const file of videoFiles) {
new Worker('./dist/worker/thumbnail.js', {
workerData: {
videos: chunk,
id: file.id,
},
}).on('error', (err) => logger.child('thumbnail').error(err));
});
}
}
+4 -4
View File
@@ -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 -1
View File
@@ -3,13 +3,15 @@ import { FastifyInstance } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { migrations } from 'server/util';
async function prismaPlugin(fastify: FastifyInstance) {
async function prismaPlugin(fastify: FastifyInstance, _, done) {
process.env.DATABASE_URL = fastify.config.core?.database_url;
await migrations();
const prisma = new PrismaClient();
fastify.decorate('prisma', prisma);
done();
}
export default fastifyPlugin(prismaPlugin, {
+9 -17
View File
@@ -1,9 +1,7 @@
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({
@@ -18,20 +16,14 @@ export default async function rawRoute(this: FastifyInstance, req: FastifyReques
if (failed) return reply.notFound();
if (file.password) {
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);
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);
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ export default async function uploadsRoute(this: FastifyInstance, req: FastifyRe
const image = await this.prisma.file.findFirst({
where: {
OR: [{ name: id }, { name: decodeURI(id) }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
OR: [{ name: 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 };
+1 -1
View File
@@ -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 };
+1 -1
View File
@@ -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 {
+58 -81
View File
@@ -1,24 +1,18 @@
import { type File, PrismaClient, type Thumbnail } from '@prisma/client';
import { File } from '@prisma/client';
import { spawn } from 'child_process';
import ffmpeg from 'ffmpeg-static';
import { createWriteStream } from 'fs';
import { rm } from 'fs/promises';
import config from 'lib/config';
import datasource from 'lib/datasource';
import Logger from 'lib/logger';
import { randomChars } from 'lib/util';
import prisma from 'lib/prisma';
import { join } from 'path';
import { isMainThread, workerData } from 'worker_threads';
import datasource from 'lib/datasource';
import config from 'lib/config';
const { videos } = workerData as {
videos: (File & {
thumbnail: Thumbnail;
})[];
};
const { id } = workerData as { id: number };
const logger = Logger.get('worker::thumbnail').child(randomChars(4));
logger.debug(`thumbnail generation for ${videos.length} videos`);
const logger = Logger.get('worker::thumbnail').child(id.toString() ?? 'unknown-ident');
if (isMainThread) {
logger.error('worker is not a thread');
@@ -30,34 +24,9 @@ async function loadThumbnail(path) {
const child = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'ignore'] });
const data: Buffer = await new Promise((resolve, reject) => {
const buffers = [];
child.stdout.on('data', (chunk) => {
buffers.push(chunk);
});
const data: Promise<Buffer> = new Promise((resolve, reject) => {
child.stdout.once('data', resolve);
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));
let offset = 0;
for (let i = 0; i !== buffers.length; ++i) {
const chunk = buffers[i];
chunk.copy(buffer, offset);
offset += chunk.length;
}
resolve(buffer);
}
});
});
return data;
@@ -80,51 +49,59 @@ async function loadFileTmp(file: File) {
}
async function start() {
const prisma = new PrismaClient();
const file = await prisma.file.findUnique({
where: {
id,
},
include: {
thumbnail: true,
},
});
for (let i = 0; i !== videos.length; ++i) {
const file = videos[i];
if (!file.mimetype.startsWith('video/')) {
logger.info('file is not a video');
process.exit(0);
}
if (file.thumbnail) {
logger.info('thumbnail already exists');
process.exit(0);
}
const tmpFile = await loadFileTmp(file);
logger.debug(`loaded file to tmp: ${tmpFile}`);
const thumbnail = await loadThumbnail(tmpFile);
logger.debug(`loaded thumbnail: ${thumbnail.length} bytes mjpeg`);
const { thumbnail: thumb } = await prisma.file.update({
where: {
id: file.id,
},
data: {
thumbnail: {
create: {
name: `.thumb-${file.id}.jpg`,
},
},
},
select: {
thumbnail: true,
},
});
await datasource.save(thumb.name, thumbnail);
logger.info(`thumbnail saved - ${thumb.name}`);
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
logger.debug(`removing tmp file: ${tmpFile}`);
await rm(tmpFile);
if (!file) {
logger.error('file not found');
process.exit(1);
}
await prisma.$disconnect();
if (!file.mimetype.startsWith('video/')) {
logger.info('file is not a video');
process.exit(0);
}
if (file.thumbnail) {
logger.info('thumbnail already exists');
process.exit(0);
}
const tmpFile = await loadFileTmp(file);
logger.debug(`loaded file to tmp: ${tmpFile}`);
const thumbnail = await loadThumbnail(tmpFile);
logger.debug(`loaded thumbnail: ${thumbnail.length} bytes mjpeg`);
const { thumbnail: thumb } = await prisma.file.update({
where: {
id: file.id,
},
data: {
thumbnail: {
create: {
name: `.thumb-${file.id}.jpg`,
},
},
},
select: {
thumbnail: true,
},
});
await datasource.save(thumb.name, thumbnail);
logger.info(`thumbnail saved - ${thumb.name}`);
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
logger.debug(`removing tmp file: ${tmpFile}`);
await rm(tmpFile);
process.exit(0);
}
+29 -23
View File
@@ -10,11 +10,11 @@ import { IncompleteFile, InvisibleFile } from '@prisma/client';
import { removeGPSData } from 'lib/utils/exif';
import { sendUpload } from 'lib/discord';
import { createInvisImage, hashPassword } from 'lib/util';
import formatFileName from 'lib/format';
export type UploadWorkerData = {
user: UserExtended;
file: {
id: number;
filename: string;
mimetype: string;
identifier: string;
@@ -24,6 +24,7 @@ export type UploadWorkerData = {
response: {
expiresAt?: Date;
format: NameFormat;
imageCompressionPercent?: number;
fileMaxViews?: number;
};
headers: Record<string, string>;
@@ -45,12 +46,7 @@ if (!file.lastchunk) {
if (!config.chunks.enabled) {
logger.error('chunks are not enabled, worker should not have been started');
if (file.id) {
prisma.file.delete({ where: { id: file.id } }).then(() => {
logger.debug('deleted a file entry due to anomalous worker start');
process.exit(1);
});
} else process.exit(1);
process.exit(1);
}
start();
@@ -59,7 +55,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) => {
@@ -79,12 +75,20 @@ async function start() {
},
});
const compressionUsed = response.imageCompressionPercent && file.mimetype.startsWith('image/');
const ext = file.filename.split('.').length === 1 ? '' : file.filename.split('.').pop();
const fileName = await formatFileName(response.format, file.filename);
let fd;
if (config.datasource.type === 'local') {
fd = await open(join(config.datasource.local.directory, file.filename), 'w');
fd = await open(
join(
config.datasource.local.directory,
`${fileName}${compressionUsed ? '.jpg' : `${ext ? '.' : ''}${ext}`}`
),
'w'
);
} else {
fd = new Uint8Array(file.totalBytes);
}
@@ -121,7 +125,10 @@ 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(
`${fileName}${compressionUsed ? '.jpg' : `${ext ? '.' : ''}${ext}`}`,
Buffer.from(fd as Uint8Array)
);
}
const final = await prisma.incompleteFile.update({
@@ -135,7 +142,7 @@ async function start() {
logger.debug('done writing file');
await runFileComplete(file.id, ext, final);
await runFileComplete(fileName, ext, compressionUsed, final);
logger.debug('done running worker');
process.exit(0);
@@ -145,11 +152,6 @@ async function setResponse(incompleteFile: IncompleteFile, code: number, message
incompleteFile.data['code'] = code;
incompleteFile.data['message'] = message;
if (code !== 200) {
await datasource.delete(file.filename);
await prisma.file.delete({ where: { id: file.id } });
}
return prisma.incompleteFile.update({
where: {
id: incompleteFile.id,
@@ -160,7 +162,12 @@ async function setResponse(incompleteFile: IncompleteFile, code: number, message
});
}
async function runFileComplete(id: number, ext: string, incompleteFile: IncompleteFile) {
async function runFileComplete(
fileName: string,
ext: string,
compressionUsed: boolean,
incompleteFile: IncompleteFile
) {
if (config.uploader.disabled_extensions.includes(ext))
return setResponse(incompleteFile, 403, 'disabled extension');
@@ -171,11 +178,11 @@ async function runFileComplete(id: number, ext: string, incompleteFile: Incomple
let invis: InvisibleFile;
const fFile = await prisma.file.update({
where: {
id,
},
const fFile = await prisma.file.create({
data: {
name: `${fileName}${compressionUsed ? '.jpg' : `${ext ? '.' : ''}${ext}`}`,
mimetype: file.mimetype,
userId: user.id,
embed: !!headers.embed,
password,
expiresAt: response.expiresAt,
@@ -185,8 +192,7 @@ async function runFileComplete(id: number, ext: string, incompleteFile: Incomple
},
});
if (typeof headers.zws !== 'undefined' && (headers.zws as string).toLowerCase().match('true'))
invis = await createInvisImage(config.uploader.length, fFile.id);
if (headers.zws) invis = await createInvisImage(config.uploader.length, fFile.id);
logger.info(`User ${user.username} (${user.id}) uploaded ${fFile.name} (${fFile.id}) (chunked)`);
let domain;
+4013 -3882
View File
File diff suppressed because it is too large Load Diff