diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/package.json b/package.json index 5c56376..34ea35e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "fflate": "^0.7.4", "find-my-way": "^7.5.0", "katex": "^0.16.4", - "mantine-datatable": "^1.8.6", + "mantine-datatable": "2.0.1", "minio": "^7.0.32", "ms": "canary", "multer": "^1.4.5-lts.1", diff --git a/src/components/File/FileModal.tsx b/src/components/File/FileModal.tsx index 45786e0..738ef15 100644 --- a/src/components/File/FileModal.tsx +++ b/src/components/File/FileModal.tsx @@ -50,6 +50,7 @@ export default function FileModal({ refresh, reducedActions = false, exifEnabled, + compress, }: { open: boolean; setOpen: (open: boolean) => void; @@ -58,6 +59,7 @@ export default function FileModal({ refresh: () => void; reducedActions?: boolean; exifEnabled?: boolean; + compress: boolean; }) { const deleteFile = useFileDelete(); const favoriteFile = useFileFavorite(); @@ -215,11 +217,11 @@ export default function FileModal({ - + setOpen(true)}> setOpen(true)} disableMediaPreview={disableMediaPreview} /> diff --git a/src/components/Theming.tsx b/src/components/Theming.tsx index 004b3ad..03cd447 100644 --- a/src/components/Theming.tsx +++ b/src/components/Theming.tsx @@ -99,6 +99,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) { }, Modal: { defaultProps: { + closeButtonProps: { size: 'lg' }, centered: true, transitionProps: { exitDuration: 100, @@ -118,7 +119,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) { }, LoadingOverlay: { defaultProps: { - overlayProps: { + overlayprops: { blur: 3, color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white', }, diff --git a/src/components/Type.tsx b/src/components/Type.tsx index 9daa7ab..7c15638 100644 --- a/src/components/Type.tsx +++ b/src/components/Type.tsx @@ -45,8 +45,8 @@ function Placeholder({ text, Icon, ...props }) { ); return ( - -
+ +
@@ -158,6 +158,8 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop image: ( } + height={320} + fit='contain' {...props} /> ), diff --git a/src/components/pages/Dashboard/RecentFiles.tsx b/src/components/pages/Dashboard/RecentFiles.tsx index 9409e1f..1dc6c2c 100644 --- a/src/components/pages/Dashboard/RecentFiles.tsx +++ b/src/components/pages/Dashboard/RecentFiles.tsx @@ -5,7 +5,7 @@ import File from 'components/File'; import MutedText from 'components/MutedText'; import { useRecent } from 'lib/queries/files'; -export default function RecentFiles({ disableMediaPreview, exifEnabled }) { +export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) { const recent = useRecent('media'); return ( @@ -25,6 +25,7 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled }) { disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} refreshImages={recent.refetch} + onDash={compress} /> )) ) : ( diff --git a/src/components/pages/Dashboard/index.tsx b/src/components/pages/Dashboard/index.tsx index f3c1156..f989486 100644 --- a/src/components/pages/Dashboard/index.tsx +++ b/src/components/pages/Dashboard/index.tsx @@ -22,7 +22,7 @@ import { useRecoilValue } from 'recoil'; import RecentFiles from './RecentFiles'; import { StatCards } from './StatCards'; -export default function Dashboard({ disableMediaPreview, exifEnabled }) { +export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) { const user = useRecoilValue(userSelector); const recent = useRecent('media'); @@ -138,6 +138,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) { refresh={() => files.refetch()} reducedActions={false} exifEnabled={exifEnabled} + compress={compress} /> )} @@ -148,7 +149,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) { - + Files diff --git a/src/components/pages/Files/FilePagation.tsx b/src/components/pages/Files/FilePagation.tsx index 7e52df3..933ed44 100644 --- a/src/components/pages/Files/FilePagation.tsx +++ b/src/components/pages/Files/FilePagation.tsx @@ -10,7 +10,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; 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 [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)); @@ -75,6 +75,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} refreshImages={pages.refetch} + onDash={compress} /> )) diff --git a/src/components/pages/Files/index.tsx b/src/components/pages/Files/index.tsx index c838c5a..d0e06e9 100644 --- a/src/components/pages/Files/index.tsx +++ b/src/components/pages/Files/index.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import { useEffect, useState } from 'react'; 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 [favoriteNumPages, setFavoriteNumPages] = useState(0); const favoritePages = usePaginatedFiles(favoritePage, 'media', true); @@ -50,6 +50,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) { disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} refreshImages={favoritePages.refetch} + onDash={compress} /> )) @@ -75,6 +76,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) { disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} queryPage={queryPage} + compress={compress} /> ); diff --git a/src/components/pages/Folders/ViewFolderFilesModal.tsx b/src/components/pages/Folders/ViewFolderFilesModal.tsx index 7ed286c..3b5016c 100644 --- a/src/components/pages/Folders/ViewFolderFilesModal.tsx +++ b/src/components/pages/Folders/ViewFolderFilesModal.tsx @@ -3,7 +3,14 @@ import File from 'components/File'; import MutedText from 'components/MutedText'; 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; const folder = useFolder(folderId, true); @@ -26,6 +33,7 @@ export default function ViewFolderFilesModal({ open, setOpen, folderId, disableM image={file} exifEnabled={exifEnabled} refreshImages={folder.refetch} + onDash={compress} /> ))} diff --git a/src/components/pages/Folders/index.tsx b/src/components/pages/Folders/index.tsx index f6dc0a3..07d15de 100644 --- a/src/components/pages/Folders/index.tsx +++ b/src/components/pages/Folders/index.tsx @@ -22,7 +22,7 @@ import { useEffect, useState } from 'react'; import CreateFolderModal from './CreateFolderModal'; import ViewFolderFilesModal from './ViewFolderFilesModal'; -export default function Folders({ disableMediaPreview, exifEnabled }) { +export default function Folders({ disableMediaPreview, exifEnabled, compress }) { const folders = useFolders(); const [createOpen, setCreateOpen] = useState(false); const [createWithFile, setCreateWithFile] = useState(null); @@ -113,6 +113,7 @@ export default function Folders({ disableMediaPreview, exifEnabled }) { folderId={activeFolderId} disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} + compress={compress} /> diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts index 12bfebc..716441e 100644 --- a/src/lib/config/Config.ts +++ b/src/lib/config/Config.ts @@ -5,11 +5,18 @@ export interface ConfigCore { port: number; database_url: string; logger: boolean; + compression: ConfigCompression; stats_interval: number; invites_interval: number; } +export interface ConfigCompression { + enabled: boolean; + threshold: number; + on_dashboard: boolean; +} + export interface ConfigDatasource { type: 'local' | 's3' | 'supabase'; local: ConfigLocalDatasource; diff --git a/src/lib/config/readConfig.ts b/src/lib/config/readConfig.ts index b2dca3f..64b941e 100644 --- a/src/lib/config/readConfig.ts +++ b/src/lib/config/readConfig.ts @@ -64,6 +64,9 @@ export default function readConfig() { map('CORE_LOGGER', 'boolean', 'core.logger'), map('CORE_STATS_INTERVAL', 'number', 'core.stats_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'), diff --git a/src/lib/config/validateConfig.ts b/src/lib/config/validateConfig.ts index 393c7b6..2d6fe5a 100644 --- a/src/lib/config/validateConfig.ts +++ b/src/lib/config/validateConfig.ts @@ -34,6 +34,16 @@ const validator = s.object({ logger: s.boolean.default(false), stats_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 .object({ diff --git a/src/lib/discord.ts b/src/lib/discord.ts index e97d5c9..745d018 100644 --- a/src/lib/discord.ts +++ b/src/lib/discord.ts @@ -23,7 +23,7 @@ export function parseContent( image: content.embed.image, } : null, - url: args[3], + url: args.link, }; } diff --git a/src/lib/middleware/getServerSideProps.ts b/src/lib/middleware/getServerSideProps.ts index 1c06ef6..9f2353f 100644 --- a/src/lib/middleware/getServerSideProps.ts +++ b/src/lib/middleware/getServerSideProps.ts @@ -64,6 +64,7 @@ export const getServerSideProps: GetServerSideProps = async (ct max_size: config.chunks.max_size, totp_enabled: config.mfa.totp_enabled, exif_enabled: config.exif.enabled, + compress: config.core.compression.on_dashboard, } as ServerSideProps, }; diff --git a/src/lib/utils/parser.ts b/src/lib/utils/parser.ts index 2827440..be0661c 100644 --- a/src/lib/utils/parser.ts +++ b/src/lib/utils/parser.ts @@ -24,11 +24,21 @@ export function parseString(str: string, value: ParseValue) { continue; } - if (matches.groups.prop in ['password', 'avatar']) { + if (['password', 'avatar'].includes(matches.groups.prop)) { str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex); re.lastIndex = matches.index; continue; } + if (['originalName', 'name'].includes(matches.groups.prop)) { + str = replaceCharsFromString( + str, + decodeURIComponent(escape(getV[matches.groups.prop])), + matches.index, + re.lastIndex + ); + re.lastIndex = matches.index; + continue; + } const v = getV[matches.groups.prop]; diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 8441cf5..1b085bd 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -22,6 +22,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { username: 'administrator', password: await hashPassword('password'), token: createToken(), + superAdmin: true, administrator: true, }, }); diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index 0370512..25dfb8e 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -72,9 +72,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { return res.json(newTarget); }); } 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(`user ${id} has ${!req.body.administrator} in administrator`); if (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({ where: { id: target.id }, data: { administrator: req.body.administrator }, diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index 50bc9f1..5f1b7af 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -99,6 +99,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { id: true, favorite: true, views: true, + folderId: true, maxViews: true, size: true, }, diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts index 344fd01..d14bcb5 100644 --- a/src/pages/api/user/recent.ts +++ b/src/pages/api/user/recent.ts @@ -26,6 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { maxViews: true, folderId: true, size: true, + favorite: true, }, }); diff --git a/src/pages/dashboard/files.tsx b/src/pages/dashboard/files.tsx index bbb7d75..7bbab07 100644 --- a/src/pages/dashboard/files.tsx +++ b/src/pages/dashboard/files.tsx @@ -22,6 +22,7 @@ export default function FilesPage(props) { disableMediaPreview={props.disable_media_preview} exifEnabled={props.exif_enabled} queryPage={props.queryPage} + compress={props.compress} /> diff --git a/src/pages/dashboard/folders.tsx b/src/pages/dashboard/folders.tsx index 76b5d76..a198588 100644 --- a/src/pages/dashboard/folders.tsx +++ b/src/pages/dashboard/folders.tsx @@ -18,7 +18,11 @@ export default function FilesPage(props) { - + ); diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index c495a7b..eb22314 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -16,7 +16,11 @@ export default function DashboardPage(props) { {props.title} - + ); diff --git a/src/pages/folder/[id].tsx b/src/pages/folder/[id].tsx index c1bf79d..fa8bfe3 100644 --- a/src/pages/folder/[id].tsx +++ b/src/pages/folder/[id].tsx @@ -25,9 +25,10 @@ type Props = { folder: LimitedFolder; uploadRoute: 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}`; return ( <> @@ -55,6 +56,7 @@ export default function Folder({ title, folder }: Props) { exifEnabled={false} refreshImages={null} reducedActions={true} + onDash={compress} /> ))} @@ -113,6 +115,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => folder, uploadRoute: config.uploader.route, title: config.website.title, + compress: config.core.compression.on_dashboard, }, }; }; diff --git a/src/pages/view/[id].tsx b/src/pages/view/[id].tsx index 9b9d870..f36202a 100644 --- a/src/pages/view/[id].tsx +++ b/src/pages/view/[id].tsx @@ -1,5 +1,6 @@ import { Box, Button, Modal, PasswordInput } from '@mantine/core'; import type { File } from '@prisma/client'; +import config from 'lib/config'; import Link from 'components/Link'; import exts from 'lib/exts'; import prisma from 'lib/prisma'; @@ -15,13 +16,18 @@ export default function EmbeddedFile({ user, pass, prismRender, + onDash, + compress, }: { - file: File; + file: File & { imageProps?: HTMLImageElement }; user: UserExtended; pass: 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 [opened, setOpened] = useState(pass); @@ -60,6 +66,7 @@ export default function EmbeddedFile({ if (url) { imageEl.src = url; } + file.imageProps = img; }; useEffect(() => { @@ -94,7 +101,11 @@ export default function EmbeddedFile({ {file.mimetype.startsWith('image') && ( <> - + + + + + )} @@ -116,6 +127,20 @@ export default function EmbeddedFile({ )} + {file.mimetype.startsWith('audio') && ( + <> + + + + + + + + + + + + )} {!file.mimetype.startsWith('video') && !file.mimetype.startsWith('image') && ( )} @@ -155,12 +180,18 @@ export default function EmbeddedFile({ )} {file.mimetype.startsWith('video') && ( - ); @@ -168,7 +199,7 @@ export default function EmbeddedFile({ export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params as { id: string }; - + const { compress = null } = context.query as unknown as { compress?: boolean }; const file = await prisma.file.findFirst({ where: { OR: [{ name: id }, { invisible: { invis: decodeURI(encodeURI(id)) } }], @@ -233,6 +264,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => { file, user, pass: file.password ? true : false, + onDash: config.core.compression.on_dashboard, + compress, }, }; }; diff --git a/src/server/decorators/rawFile.ts b/src/server/decorators/rawFile.ts index 7f2b121..8078345 100644 --- a/src/server/decorators/rawFile.ts +++ b/src/server/decorators/rawFile.ts @@ -2,26 +2,65 @@ import { FastifyInstance, FastifyReply } from 'fastify'; import { guess } from 'lib/mimes'; import { extname } from 'path'; import fastifyPlugin from 'fastify-plugin'; +import { createBrotliCompress, createDeflate, createGzip } from 'zlib'; +import pump from 'pump'; +import { Transform } from 'stream'; function rawFileDecorator(fastify: FastifyInstance, _, done) { fastify.decorateReply('rawFile', rawFile); done(); 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); if (!data) return this.notFound(); const mimetype = await guess(extname(id).slice(1)); const size = await this.server.datasource.size(id); - - this.header('Content-Length', size); 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); } } +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, { name: 'rawFile', decorators: { diff --git a/yarn.lock b/yarn.lock index b349c09..b90009c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7167,14 +7167,14 @@ __metadata: languageName: node linkType: hard -"mantine-datatable@npm:^1.8.6": - version: 1.8.6 - resolution: "mantine-datatable@npm:1.8.6" +"mantine-datatable@npm:2.0.1": + version: 2.0.1 + resolution: "mantine-datatable@npm:2.0.1" peerDependencies: - "@mantine/core": ^5.10.4 - "@mantine/hooks": ^5.10.4 + "@mantine/core": ^6.0.0 + "@mantine/hooks": ^6.0.0 react: ^18.2.0 - checksum: a4558418067ada3e269e3d9dc0153b613b031ecd5fccd484b45a2b13e403084971870515d51c888d933eb2f4d85f6df2ad221b0c84a2bf2cd602d27938bb9029 + checksum: aa39a662619373a82ed0408ffbe4ade71449abf281495af6820bfe9113d6de22005128d8ac1ff7d6a89f4919d58c1c2ceb0fdfa4e9be37050350d2635f595f6b languageName: node linkType: hard @@ -11729,7 +11729,7 @@ __metadata: fflate: ^0.7.4 find-my-way: ^7.5.0 katex: ^0.16.4 - mantine-datatable: ^1.8.6 + mantine-datatable: 2.0.1 minio: ^7.0.32 ms: canary multer: ^1.4.5-lts.1