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

View file

@ -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({
<Stack>
<Type
file={file}
src={`/r/${encodeURI(file.name)}`}
src={`/r/${encodeURI(file.name)}?compress=${compress}`}
alt={file.name}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
sx={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
style={{ minHeight: 200, overflowY: 'scroll', maxHeight: 500 }}
disableMediaPreview={false}
overrideRender={overrideRender}
setOverrideRender={setOverrideRender}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }) {
<StatCards />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
<Box my='sm'>
<Title>Files</Title>

View file

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

View file

@ -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}
/>
</div>
))
@ -75,6 +76,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
queryPage={queryPage}
compress={compress}
/>
</>
);

View file

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

View file

@ -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}
/>
<Group mb='md'>

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ export function parseContent(
image: content.embed.image,
}
: 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,
totp_enabled: config.mfa.totp_enabled,
exif_enabled: config.exif.enabled,
compress: config.core.compression.on_dashboard,
} as ServerSideProps,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,11 @@ export default function FilesPage(props) {
</Head>
<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>
</>
);

View file

@ -16,7 +16,11 @@ export default function DashboardPage(props) {
<title>{props.title}</title>
</Head>
<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>
</>
);

View file

@ -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}
/>
))}
</SimpleGrid>
@ -113,6 +115,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
folder,
uploadRoute: config.uploader.route,
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 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') && (
<>
<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' />
</>
)}
@ -116,6 +127,20 @@ export default function EmbeddedFile({
<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') && (
<meta property='og:url' content={`/r/${file.name}`} />
)}
@ -155,10 +180,16 @@ export default function EmbeddedFile({
)}
{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>
)}
</Box>
@ -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,
},
};
};

View file

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

View file

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