feat: multiple stuffs

This commit is contained in:
Jayvin Hernandez 2023-03-18 20:52:04 -07:00 committed by GitHub
parent 9c5b3f60d5
commit 5b9b454330
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 185 additions and 41 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

View file

@ -55,7 +55,7 @@
"fflate": "^0.7.4", "fflate": "^0.7.4",
"find-my-way": "^7.5.0", "find-my-way": "^7.5.0",
"katex": "^0.16.4", "katex": "^0.16.4",
"mantine-datatable": "^1.8.6", "mantine-datatable": "2.0.1",
"minio": "^7.0.32", "minio": "^7.0.32",
"ms": "canary", "ms": "canary",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",

View file

@ -50,6 +50,7 @@ export default function FileModal({
refresh, refresh,
reducedActions = false, reducedActions = false,
exifEnabled, exifEnabled,
compress,
}: { }: {
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
@ -58,6 +59,7 @@ export default function FileModal({
refresh: () => void; refresh: () => void;
reducedActions?: boolean; reducedActions?: boolean;
exifEnabled?: boolean; exifEnabled?: boolean;
compress: boolean;
}) { }) {
const deleteFile = useFileDelete(); const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite(); const favoriteFile = useFileFavorite();
@ -215,11 +217,11 @@ export default function FileModal({
<Stack> <Stack>
<Type <Type
file={file} file={file}
src={`/r/${encodeURI(file.name)}`} src={`/r/${encodeURI(file.name)}?compress=${compress}`}
alt={file.name} alt={file.name}
popup popup
sx={{ minHeight: 200 }} sx={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
style={{ minHeight: 200 }} style={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
disableMediaPreview={false} disableMediaPreview={false}
overrideRender={overrideRender} overrideRender={overrideRender}
setOverrideRender={setOverrideRender} setOverrideRender={setOverrideRender}

View file

@ -34,6 +34,7 @@ export default function File({
exifEnabled, exifEnabled,
refreshImages, refreshImages,
reducedActions = false, reducedActions = false,
onDash,
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const deleteFile = useFileDelete(); const deleteFile = useFileDelete();
@ -57,9 +58,10 @@ export default function File({
refresh={refresh} refresh={refresh}
reducedActions={reducedActions} reducedActions={reducedActions}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
compress={onDash}
/> />
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'> <Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md' onClick={() => setOpen(true)}>
<Card.Section> <Card.Section>
<LoadingOverlay visible={loading} /> <LoadingOverlay visible={loading} />
<Type <Type
@ -78,9 +80,8 @@ export default function File({
width: '100%', width: '100%',
cursor: 'pointer', cursor: 'pointer',
}} }}
src={`/r/${encodeURI(image.name)}`} src={`/r/${encodeURI(image.name)}?compress=${onDash}`}
alt={image.name} alt={image.name}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
/> />
</Card.Section> </Card.Section>

View file

@ -99,6 +99,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
}, },
Modal: { Modal: {
defaultProps: { defaultProps: {
closeButtonProps: { size: 'lg' },
centered: true, centered: true,
transitionProps: { transitionProps: {
exitDuration: 100, exitDuration: 100,
@ -118,7 +119,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
}, },
LoadingOverlay: { LoadingOverlay: {
defaultProps: { defaultProps: {
overlayProps: { overlayprops: {
blur: 3, blur: 3,
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white', color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
}, },

View file

@ -45,8 +45,8 @@ function Placeholder({ text, Icon, ...props }) {
); );
return ( return (
<Box sx={{ height: 200 }} {...props}> <Box sx={{ height: 320 }} {...props}>
<Center sx={{ height: 200 }}> <Center sx={{ height: 320 }}>
<PlaceholderContent text={text} Icon={Icon} /> <PlaceholderContent text={text} Icon={Icon} />
</Center> </Center>
</Box> </Box>
@ -158,6 +158,8 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
image: ( image: (
<Image <Image
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />} placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
height={320}
fit='contain'
{...props} {...props}
/> />
), ),

View file

@ -5,7 +5,7 @@ import File from 'components/File';
import MutedText from 'components/MutedText'; import MutedText from 'components/MutedText';
import { useRecent } from 'lib/queries/files'; import { useRecent } from 'lib/queries/files';
export default function RecentFiles({ disableMediaPreview, exifEnabled }) { export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) {
const recent = useRecent('media'); const recent = useRecent('media');
return ( return (
@ -25,6 +25,7 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled }) {
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
refreshImages={recent.refetch} refreshImages={recent.refetch}
onDash={compress}
/> />
)) ))
) : ( ) : (

View file

@ -22,7 +22,7 @@ import { useRecoilValue } from 'recoil';
import RecentFiles from './RecentFiles'; import RecentFiles from './RecentFiles';
import { StatCards } from './StatCards'; import { StatCards } from './StatCards';
export default function Dashboard({ disableMediaPreview, exifEnabled }) { export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) {
const user = useRecoilValue(userSelector); const user = useRecoilValue(userSelector);
const recent = useRecent('media'); const recent = useRecent('media');
@ -138,6 +138,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
refresh={() => files.refetch()} refresh={() => files.refetch()}
reducedActions={false} reducedActions={false}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
compress={compress}
/> />
)} )}
@ -148,7 +149,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
<StatCards /> <StatCards />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} /> <RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
<Box my='sm'> <Box my='sm'>
<Title>Files</Title> <Title>Files</Title>

View file

@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage }) { export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [checked, setChecked] = useRecoilState(showNonMediaSelector); const [checked, setChecked] = useRecoilState(showNonMediaSelector);
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
const [page, setPage] = useState(Number(queryPage)); const [page, setPage] = useState(Number(queryPage));
@ -75,6 +75,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
refreshImages={pages.refetch} refreshImages={pages.refetch}
onDash={compress}
/> />
</div> </div>
)) ))

View file

@ -7,7 +7,7 @@ import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import FilePagation from './FilePagation'; import FilePagation from './FilePagation';
export default function Files({ disableMediaPreview, exifEnabled, queryPage }) { export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [favoritePage, setFavoritePage] = useState(1); const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0); const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true); const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
@ -50,6 +50,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
refreshImages={favoritePages.refetch} refreshImages={favoritePages.refetch}
onDash={compress}
/> />
</div> </div>
)) ))
@ -75,6 +76,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
queryPage={queryPage} queryPage={queryPage}
compress={compress}
/> />
</> </>
); );

View file

@ -3,7 +3,14 @@ import File from 'components/File';
import MutedText from 'components/MutedText'; import MutedText from 'components/MutedText';
import { useFolder } from 'lib/queries/folders'; import { useFolder } from 'lib/queries/folders';
export default function ViewFolderFilesModal({ open, setOpen, folderId, disableMediaPreview, exifEnabled }) { export default function ViewFolderFilesModal({
open,
setOpen,
folderId,
disableMediaPreview,
exifEnabled,
compress,
}) {
if (!folderId) return null; if (!folderId) return null;
const folder = useFolder(folderId, true); const folder = useFolder(folderId, true);
@ -26,6 +33,7 @@ export default function ViewFolderFilesModal({ open, setOpen, folderId, disableM
image={file} image={file}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
refreshImages={folder.refetch} refreshImages={folder.refetch}
onDash={compress}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>

View file

@ -22,7 +22,7 @@ import { useEffect, useState } from 'react';
import CreateFolderModal from './CreateFolderModal'; import CreateFolderModal from './CreateFolderModal';
import ViewFolderFilesModal from './ViewFolderFilesModal'; import ViewFolderFilesModal from './ViewFolderFilesModal';
export default function Folders({ disableMediaPreview, exifEnabled }) { export default function Folders({ disableMediaPreview, exifEnabled, compress }) {
const folders = useFolders(); const folders = useFolders();
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [createWithFile, setCreateWithFile] = useState(null); const [createWithFile, setCreateWithFile] = useState(null);
@ -113,6 +113,7 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
folderId={activeFolderId} folderId={activeFolderId}
disableMediaPreview={disableMediaPreview} disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled} exifEnabled={exifEnabled}
compress={compress}
/> />
<Group mb='md'> <Group mb='md'>

View file

@ -5,11 +5,18 @@ export interface ConfigCore {
port: number; port: number;
database_url: string; database_url: string;
logger: boolean; logger: boolean;
compression: ConfigCompression;
stats_interval: number; stats_interval: number;
invites_interval: number; invites_interval: number;
} }
export interface ConfigCompression {
enabled: boolean;
threshold: number;
on_dashboard: boolean;
}
export interface ConfigDatasource { export interface ConfigDatasource {
type: 'local' | 's3' | 'supabase'; type: 'local' | 's3' | 'supabase';
local: ConfigLocalDatasource; local: ConfigLocalDatasource;

View file

@ -64,6 +64,9 @@ export default function readConfig() {
map('CORE_LOGGER', 'boolean', 'core.logger'), map('CORE_LOGGER', 'boolean', 'core.logger'),
map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'), map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'),
map('CORE_INVITES_INTERVAL', 'number', 'core.invites_interval'), map('CORE_INVITES_INTERVAL', 'number', 'core.invites_interval'),
map('CORE_COMPRESSION_ENABLED', 'boolean', 'core.compression.enabled'),
map('CORE_COMPRESSION_THRESHOLD', 'human-to-byte', 'core.compression.threshold'),
map('CORE_COMPRESSION_ON_DASHBOARD', 'boolean', 'core.compression.on_dashboard'),
map('DATASOURCE_TYPE', 'string', 'datasource.type'), map('DATASOURCE_TYPE', 'string', 'datasource.type'),

View file

@ -34,6 +34,16 @@ const validator = s.object({
logger: s.boolean.default(false), logger: s.boolean.default(false),
stats_interval: s.number.default(1800), stats_interval: s.number.default(1800),
invites_interval: s.number.default(1800), invites_interval: s.number.default(1800),
compression: s
.object({
enabled: s.boolean.default(false),
threshold: s.number.default(2048),
on_dashboard: s.boolean.default(true),
})
.default({
enabled: false,
on_dashboard: false,
}),
}), }),
datasource: s datasource: s
.object({ .object({

View file

@ -23,7 +23,7 @@ export function parseContent(
image: content.embed.image, image: content.embed.image,
} }
: null, : null,
url: args[3], url: args.link,
}; };
} }

View file

@ -64,6 +64,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
max_size: config.chunks.max_size, max_size: config.chunks.max_size,
totp_enabled: config.mfa.totp_enabled, totp_enabled: config.mfa.totp_enabled,
exif_enabled: config.exif.enabled, exif_enabled: config.exif.enabled,
compress: config.core.compression.on_dashboard,
} as ServerSideProps, } as ServerSideProps,
}; };

View file

@ -24,11 +24,21 @@ export function parseString(str: string, value: ParseValue) {
continue; continue;
} }
if (matches.groups.prop in ['password', 'avatar']) { if (['password', 'avatar'].includes(matches.groups.prop)) {
str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex); str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
re.lastIndex = matches.index; re.lastIndex = matches.index;
continue; continue;
} }
if (['originalName', 'name'].includes(matches.groups.prop)) {
str = replaceCharsFromString(
str,
decodeURIComponent(escape(getV[matches.groups.prop])),
matches.index,
re.lastIndex
);
re.lastIndex = matches.index;
continue;
}
const v = getV[matches.groups.prop]; const v = getV[matches.groups.prop];

View file

@ -22,6 +22,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
username: 'administrator', username: 'administrator',
password: await hashPassword('password'), password: await hashPassword('password'),
token: createToken(), token: createToken(),
superAdmin: true,
administrator: true, administrator: true,
}, },
}); });

View file

@ -72,9 +72,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json(newTarget); return res.json(newTarget);
}); });
} else if (req.method === 'PATCH') { } else if (req.method === 'PATCH') {
if (target.administrator && !user.superAdmin) return res.forbidden('cannot modify administrator'); if (
(target.administrator && !user.superAdmin) ||
(target.administrator && user.administrator && !user.superAdmin)
)
return res.forbidden('cannot modify administrator');
logger.debug(`attempting to update user ${id} with ${JSON.stringify(req.body)}`); logger.debug(`attempting to update user ${id} with ${JSON.stringify(req.body)}`);
logger.debug(`user ${id} has ${!req.body.administrator} in administrator`);
if (req.body.password) { if (req.body.password) {
const hashed = await hashPassword(req.body.password); const hashed = await hashPassword(req.body.password);
@ -84,7 +89,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
}); });
} }
if (req.body.administrator) { if (typeof req.body.administrator != 'undefined') {
await prisma.user.update({ await prisma.user.update({
where: { id: target.id }, where: { id: target.id },
data: { administrator: req.body.administrator }, data: { administrator: req.body.administrator },

View file

@ -99,6 +99,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
id: true, id: true,
favorite: true, favorite: true,
views: true, views: true,
folderId: true,
maxViews: true, maxViews: true,
size: true, size: true,
}, },

View file

@ -26,6 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
maxViews: true, maxViews: true,
folderId: true, folderId: true,
size: true, size: true,
favorite: true,
}, },
}); });

View file

@ -22,6 +22,7 @@ export default function FilesPage(props) {
disableMediaPreview={props.disable_media_preview} disableMediaPreview={props.disable_media_preview}
exifEnabled={props.exif_enabled} exifEnabled={props.exif_enabled}
queryPage={props.queryPage} queryPage={props.queryPage}
compress={props.compress}
/> />
</Layout> </Layout>
</> </>

View file

@ -18,7 +18,11 @@ export default function FilesPage(props) {
</Head> </Head>
<Layout props={props}> <Layout props={props}>
<Folders disableMediaPreview={props.disable_media_preview} exifEnabled={props.exif_enabled} /> <Folders
disableMediaPreview={props.disable_media_preview}
exifEnabled={props.exif_enabled}
compress={props.compress}
/>
</Layout> </Layout>
</> </>
); );

View file

@ -16,7 +16,11 @@ export default function DashboardPage(props) {
<title>{props.title}</title> <title>{props.title}</title>
</Head> </Head>
<Layout props={props}> <Layout props={props}>
<Dashboard disableMediaPreview={props.disable_media_preview} exifEnabled={props.exif_enabled} /> <Dashboard
disableMediaPreview={props.disable_media_preview}
exifEnabled={props.exif_enabled}
compress={props.compress}
/>
</Layout> </Layout>
</> </>
); );

View file

@ -25,9 +25,10 @@ type Props = {
folder: LimitedFolder; folder: LimitedFolder;
uploadRoute: string; uploadRoute: string;
title: string; title: string;
compress: boolean;
}; };
export default function Folder({ title, folder }: Props) { export default function Folder({ title, folder, compress }: Props) {
const full_title = `${title} - ${folder.name}`; const full_title = `${title} - ${folder.name}`;
return ( return (
<> <>
@ -55,6 +56,7 @@ export default function Folder({ title, folder }: Props) {
exifEnabled={false} exifEnabled={false}
refreshImages={null} refreshImages={null}
reducedActions={true} reducedActions={true}
onDash={compress}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
@ -113,6 +115,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
folder, folder,
uploadRoute: config.uploader.route, uploadRoute: config.uploader.route,
title: config.website.title, title: config.website.title,
compress: config.core.compression.on_dashboard,
}, },
}; };
}; };

View file

@ -1,5 +1,6 @@
import { Box, Button, Modal, PasswordInput } from '@mantine/core'; import { Box, Button, Modal, PasswordInput } from '@mantine/core';
import type { File } from '@prisma/client'; import type { File } from '@prisma/client';
import config from 'lib/config';
import Link from 'components/Link'; import Link from 'components/Link';
import exts from 'lib/exts'; import exts from 'lib/exts';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
@ -15,13 +16,18 @@ export default function EmbeddedFile({
user, user,
pass, pass,
prismRender, prismRender,
onDash,
compress,
}: { }: {
file: File; file: File & { imageProps?: HTMLImageElement };
user: UserExtended; user: UserExtended;
pass: boolean; pass: boolean;
prismRender: boolean; prismRender: boolean;
onDash: boolean;
compress?: boolean;
}) { }) {
const dataURL = (route: string) => `${route}/${encodeURI(file.name)}`; const dataURL = (route: string) =>
`${route}/${encodeURI(file.name)}?compress=${compress == null ? onDash : compress}`;
const router = useRouter(); const router = useRouter();
const [opened, setOpened] = useState(pass); const [opened, setOpened] = useState(pass);
@ -60,6 +66,7 @@ export default function EmbeddedFile({
if (url) { if (url) {
imageEl.src = url; imageEl.src = url;
} }
file.imageProps = img;
}; };
useEffect(() => { useEffect(() => {
@ -94,7 +101,11 @@ export default function EmbeddedFile({
{file.mimetype.startsWith('image') && ( {file.mimetype.startsWith('image') && (
<> <>
<meta property='og:image' content={`/r/${file.name}`} /> <meta property='og:type' content='image' />
<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:card' content='summary_large_image' />
</> </>
)} )}
@ -116,6 +127,20 @@ export default function EmbeddedFile({
<meta property='og:video:height' content='480' /> <meta property='og:video:height' content='480' />
</> </>
)} )}
{file.mimetype.startsWith('audio') && (
<>
<meta name='twitter:card' content='player' />
<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 property='og:type' content='music.song' />
<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') && ( {!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && (
<meta property='og:url' content={`/r/${file.name}`} /> <meta property='og:url' content={`/r/${file.name}`} />
)} )}
@ -155,10 +180,16 @@ export default function EmbeddedFile({
)} )}
{file.mimetype.startsWith('video') && ( {file.mimetype.startsWith('video') && (
<video src={dataURL('/r')} controls={true} autoPlay={true} id='image_content' /> <video src={dataURL('/r')} controls autoPlay muted id='video_content' />
)} )}
{!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && ( {file.mimetype.startsWith('audio') && (
<audio src={dataURL('/r')} controls autoPlay muted id='audio_content' />
)}
{!file.mimetype.startsWith('video') &&
!file.mimetype.startsWith('image') &&
!file.mimetype.startsWith('audio') && (
<Link href={dataURL('/r')}>Can&#39;t preview this file. Click here to download it.</Link> <Link href={dataURL('/r')}>Can&#39;t preview this file. Click here to download it.</Link>
)} )}
</Box> </Box>
@ -168,7 +199,7 @@ export default function EmbeddedFile({
export const getServerSideProps: GetServerSideProps = async (context) => { export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params as { id: string }; const { id } = context.params as { id: string };
const { compress = null } = context.query as unknown as { compress?: boolean };
const file = await prisma.file.findFirst({ const file = await prisma.file.findFirst({
where: { where: {
OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }], OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }],
@ -233,6 +264,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
file, file,
user, user,
pass: file.password ? true : false, pass: file.password ? true : false,
onDash: config.core.compression.on_dashboard,
compress,
}, },
}; };
}; };

View file

@ -2,26 +2,65 @@ import { FastifyInstance, FastifyReply } from 'fastify';
import { guess } from 'lib/mimes'; import { guess } from 'lib/mimes';
import { extname } from 'path'; import { extname } from 'path';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
import { createBrotliCompress, createDeflate, createGzip } from 'zlib';
import pump from 'pump';
import { Transform } from 'stream';
function rawFileDecorator(fastify: FastifyInstance, _, done) { function rawFileDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('rawFile', rawFile); fastify.decorateReply('rawFile', rawFile);
done(); done();
async function rawFile(this: FastifyReply, id: string) { async function rawFile(this: FastifyReply, id: string) {
const { download } = this.request.query as { download?: string }; const { download, compress } = this.request.query as { download?: string; compress?: boolean };
const data = await this.server.datasource.get(id); const data = await this.server.datasource.get(id);
if (!data) return this.notFound(); if (!data) return this.notFound();
const mimetype = await guess(extname(id).slice(1)); const mimetype = await guess(extname(id).slice(1));
const size = await this.server.datasource.size(id); const size = await this.server.datasource.size(id);
this.header('Content-Length', size);
this.header('Content-Type', download ? 'application/octet-stream' : mimetype); this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
if (
this.server.config.core.compression.enabled &&
compress &&
!this.request.headers['X-Zipline-NoCompress'] &&
!!this.request.headers['accept-encoding']
)
if (size > this.server.config.core.compression.threshold)
return this.send(useCompress.call(this, data));
this.header('Content-Length', size);
return this.send(data); return this.send(data);
} }
} }
function useCompress(this: FastifyReply, data: NodeJS.ReadableStream) {
let compress: Transform;
switch ((this.request.headers['accept-encoding'] as string).split(', ')[0]) {
case 'gzip':
case 'x-gzip':
compress = createGzip();
this.header('Content-Encoding', 'gzip');
break;
case 'deflate':
compress = createDeflate();
this.header('Content-Encoding', 'deflate');
break;
case 'br':
compress = createBrotliCompress();
this.header('Content-Encoding', 'br');
break;
default:
this.server.logger
.child('response')
.error(`Unsupported encoding: ${this.request.headers['accept-encoding']}}`);
break;
}
if (!compress) return data;
setTimeout(() => compress.destroy(), 2000);
return pump(data, compress, (err) => (err ? this.server.logger.error(err) : null));
}
export default fastifyPlugin(rawFileDecorator, { export default fastifyPlugin(rawFileDecorator, {
name: 'rawFile', name: 'rawFile',
decorators: { decorators: {

View file

@ -7167,14 +7167,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mantine-datatable@npm:^1.8.6": "mantine-datatable@npm:2.0.1":
version: 1.8.6 version: 2.0.1
resolution: "mantine-datatable@npm:1.8.6" resolution: "mantine-datatable@npm:2.0.1"
peerDependencies: peerDependencies:
"@mantine/core": ^5.10.4 "@mantine/core": ^6.0.0
"@mantine/hooks": ^5.10.4 "@mantine/hooks": ^6.0.0
react: ^18.2.0 react: ^18.2.0
checksum: a4558418067ada3e269e3d9dc0153b613b031ecd5fccd484b45a2b13e403084971870515d51c888d933eb2f4d85f6df2ad221b0c84a2bf2cd602d27938bb9029 checksum: aa39a662619373a82ed0408ffbe4ade71449abf281495af6820bfe9113d6de22005128d8ac1ff7d6a89f4919d58c1c2ceb0fdfa4e9be37050350d2635f595f6b
languageName: node languageName: node
linkType: hard linkType: hard
@ -11729,7 +11729,7 @@ __metadata:
fflate: ^0.7.4 fflate: ^0.7.4
find-my-way: ^7.5.0 find-my-way: ^7.5.0
katex: ^0.16.4 katex: ^0.16.4
mantine-datatable: ^1.8.6 mantine-datatable: 2.0.1
minio: ^7.0.32 minio: ^7.0.32
ms: canary ms: canary
multer: ^1.4.5-lts.1 multer: ^1.4.5-lts.1