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
26 changed files with 230 additions and 457 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: diced

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

View File

@@ -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",

View File

@@ -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'

View File

@@ -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>
);
}

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,
});

View File

@@ -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>
)}
</>

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,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&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);
}

View File

@@ -123,8 +123,6 @@ export interface ConfigFeatures {
default_avatar: string;
robots_txt: string;
thumbnails: boolean;
}
export interface ConfigOAuth {

View File

@@ -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'),

View File

@@ -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({

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);

View File

@@ -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();

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,
},
});
@@ -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);
}

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,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})`

View File

@@ -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,
},
};

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);

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) {

View File

@@ -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();

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();

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.'
)
: console.log('Done.');
process.exit(0);
}

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();

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,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,
},
});
}

View File

@@ -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);
}

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();
@@ -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;

View File

@@ -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