mirror of
https://github.com/diced/zipline.git
synced 2025-12-30 06:31:08 -08:00
Compare commits
5 Commits
v3.7.1
...
feature/vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88fdb2fcc1 | ||
|
|
e92d78f671 | ||
|
|
bcd2897c4e | ||
|
|
24dacb478d | ||
|
|
4893f4a09e |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: diced
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"version": "3.7.1",
|
||||
"version": "3.7.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
|
||||
@@ -349,11 +349,7 @@ 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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||
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,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,10 +62,10 @@ 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);
|
||||
};
|
||||
@@ -138,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‘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);
|
||||
}
|
||||
|
||||
@@ -123,8 +123,6 @@ export interface ConfigFeatures {
|
||||
default_avatar: string;
|
||||
|
||||
robots_txt: string;
|
||||
|
||||
thumbnails: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigOAuth {
|
||||
|
||||
@@ -163,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'),
|
||||
|
||||
@@ -191,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,
|
||||
@@ -202,7 +201,6 @@ const validator = s.object({
|
||||
headless: false,
|
||||
default_avatar: null,
|
||||
robots_txt: false,
|
||||
thumbnails: false,
|
||||
}),
|
||||
chunks: s
|
||||
.object({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -109,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,
|
||||
@@ -148,6 +122,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
response: {
|
||||
expiresAt: expiry,
|
||||
format,
|
||||
imageCompressionPercent,
|
||||
fileMaxViews,
|
||||
},
|
||||
headers: req.headers,
|
||||
@@ -156,7 +131,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
return res.json({
|
||||
pending: true,
|
||||
files: [responseUrl],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,8 +226,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,12 +63,10 @@ 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})`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
||||
import type { File, Thumbnail } from '@prisma/client';
|
||||
import type { File } from '@prisma/client';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import exts from 'lib/exts';
|
||||
import prisma from 'lib/prisma';
|
||||
@@ -10,21 +10,18 @@ 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) => `${route}/${encodeURI(file.name)}?compress=${compress ?? false}`;
|
||||
@@ -102,36 +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: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' />
|
||||
@@ -140,22 +127,19 @@ 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>
|
||||
@@ -218,27 +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 };
|
||||
|
||||
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,
|
||||
@@ -269,7 +235,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
user,
|
||||
pass,
|
||||
prismRender: true,
|
||||
host,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -287,7 +252,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
props: {
|
||||
file,
|
||||
user,
|
||||
host,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -300,7 +264,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
file,
|
||||
user,
|
||||
pass: file.password ? true : false,
|
||||
host,
|
||||
compress,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -52,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 +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();
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
: console.log('Done.');
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,6 +21,7 @@ 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');
|
||||
@@ -184,12 +184,11 @@ Disallow: ${config.urls.route}
|
||||
|
||||
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) {
|
||||
@@ -229,38 +228,14 @@ 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,
|
||||
config,
|
||||
datasource,
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,25 +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 type { Config } from 'lib/config/Config';
|
||||
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';
|
||||
|
||||
const { videos, config } = workerData as {
|
||||
videos: (File & {
|
||||
thumbnail: Thumbnail;
|
||||
})[];
|
||||
config: Config;
|
||||
};
|
||||
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');
|
||||
@@ -31,30 +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) {
|
||||
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;
|
||||
@@ -77,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
@@ -3841,9 +3841,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001406":
|
||||
version: 1.0.30001494
|
||||
resolution: "caniuse-lite@npm:1.0.30001494"
|
||||
checksum: 770b742ebba6076da72e94f979ef609bbc855369d1b937c52227935d966b11c3b02baa6511fba04a804802b6eb22af0a2a4a82405963bbb769772530e6be7a8e
|
||||
version: 1.0.30001439
|
||||
resolution: "caniuse-lite@npm:1.0.30001439"
|
||||
checksum: 3912dd536c9735713ca85e47721988bbcefb881ddb4886b0b9923fa984247fd22cba032cf268e57d158af0e8a2ae2eae042ae01942a1d6d7849fa9fa5d62fb82
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user