mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 10:43:06 -07:00
feat: anonymous folder uploads
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Folder" ADD COLUMN "allowUploads" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -284,6 +284,7 @@ model Folder {
|
||||
|
||||
name String
|
||||
public Boolean @default(false)
|
||||
allowUploads Boolean @default(false)
|
||||
|
||||
files File[]
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { uploadPartialFiles } from '../uploadPartialFiles';
|
||||
import { humanizeDuration } from '@/lib/relativeTime';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function UploadFile() {
|
||||
export default function UploadFile({ title, folder }: { title?: string; folder?: string }) {
|
||||
const theme = useMantineTheme();
|
||||
const colorScheme = useColorScheme();
|
||||
const clipboard = useClipboard();
|
||||
@@ -85,6 +85,7 @@ export default function UploadFile() {
|
||||
options,
|
||||
ephemeral,
|
||||
config,
|
||||
folder,
|
||||
});
|
||||
} else {
|
||||
const size = aggSize();
|
||||
@@ -111,6 +112,7 @@ export default function UploadFile() {
|
||||
clearEphemeral,
|
||||
options,
|
||||
ephemeral,
|
||||
folder,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -126,13 +128,15 @@ export default function UploadFile() {
|
||||
return (
|
||||
<>
|
||||
<Group gap='sm'>
|
||||
<Title order={1}>Upload files</Title>
|
||||
<Title order={1}>{title ?? 'Upload files'}</Title>
|
||||
|
||||
<Tooltip label='View your files'>
|
||||
<ActionIcon component={Link} href='/dashboard/files' variant='outline' radius='sm'>
|
||||
<IconFiles size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!folder && (
|
||||
<Tooltip label='View your files'>
|
||||
<ActionIcon component={Link} href='/dashboard/files' variant='outline' radius='sm'>
|
||||
<IconFiles size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Dropzone
|
||||
@@ -212,7 +216,7 @@ export default function UploadFile() {
|
||||
</Grid>
|
||||
|
||||
<Group justify='right' gap='sm' my='md'>
|
||||
<UploadOptionsButton numFiles={files.length} />
|
||||
<UploadOptionsButton folder={folder} numFiles={files.length} />
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
|
||||
@@ -36,7 +36,7 @@ import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function UploadOptionsButton({ numFiles }: { numFiles: number }) {
|
||||
export default function UploadOptionsButton({ folder, numFiles }: { folder?: string; numFiles: number }) {
|
||||
const config = useConfig();
|
||||
|
||||
const [opened, setOpen] = useState(false);
|
||||
@@ -65,14 +65,14 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
|
||||
const combobox = useCombobox();
|
||||
const [folderSearch, setFolderSearch] = useState('');
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
useUploadOptionsStore.subscribe(
|
||||
(state) => state.ephemeral,
|
||||
(current) => (current.folderId === null ? setFolderSearch('') : null),
|
||||
),
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (folder) return;
|
||||
|
||||
useUploadOptionsStore.subscribe(
|
||||
(state) => state.ephemeral,
|
||||
(current) => (current.folderId === null ? setFolderSearch('') : null),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -236,6 +236,7 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
|
||||
setEphemeral('folderId', value === 'no folder' || value === '' ? null : value);
|
||||
combobox.closeDropdown();
|
||||
}}
|
||||
disabled={!!folder}
|
||||
>
|
||||
<Combobox.Target>
|
||||
<InputBase
|
||||
|
||||
@@ -103,6 +103,7 @@ export function uploadFiles(
|
||||
clearEphemeral,
|
||||
options,
|
||||
ephemeral,
|
||||
folder,
|
||||
}: {
|
||||
setProgress: (o: { percent: number; remaining: number; speed: number }) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
@@ -111,6 +112,7 @@ export function uploadFiles(
|
||||
clearEphemeral: () => void;
|
||||
options: UploadOptionsStore['options'];
|
||||
ephemeral: UploadOptionsStore['ephemeral'];
|
||||
folder?: string;
|
||||
},
|
||||
) {
|
||||
setLoading(true);
|
||||
@@ -193,7 +195,12 @@ export function uploadFiles(
|
||||
|
||||
ephemeral.password && req.setRequestHeader('x-zipline-password', ephemeral.password);
|
||||
ephemeral.filename && req.setRequestHeader('x-zipline-filename', encodeURIComponent(ephemeral.filename));
|
||||
ephemeral.folderId && req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
|
||||
|
||||
if (folder) {
|
||||
req.setRequestHeader('x-zipline-folder', folder);
|
||||
} else if (ephemeral.folderId) {
|
||||
req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
|
||||
}
|
||||
|
||||
req.send(body);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export async function uploadPartialFiles(
|
||||
options,
|
||||
ephemeral,
|
||||
config,
|
||||
folder,
|
||||
}: {
|
||||
setProgress: (o: { percent: number; remaining: number; speed: number }) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
@@ -90,6 +91,7 @@ export async function uploadPartialFiles(
|
||||
options: UploadOptionsStore['options'];
|
||||
ephemeral: UploadOptionsStore['ephemeral'];
|
||||
config: ReturnType<typeof useConfig>;
|
||||
folder?: string;
|
||||
},
|
||||
) {
|
||||
setLoading(true);
|
||||
@@ -249,7 +251,12 @@ export async function uploadPartialFiles(
|
||||
ephemeral.password && req.setRequestHeader('x-zipline-password', ephemeral.password);
|
||||
ephemeral.filename &&
|
||||
req.setRequestHeader('x-zipline-filename', encodeURIComponent(ephemeral.filename));
|
||||
ephemeral.folderId && req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
|
||||
|
||||
if (folder) {
|
||||
req.setRequestHeader('x-zipline-folder', folder);
|
||||
} else if (ephemeral.folderId) {
|
||||
req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
|
||||
}
|
||||
|
||||
req.setRequestHeader('x-zipline-p-identifier', identifier);
|
||||
req.setRequestHeader('x-zipline-p-filename', encodeURIComponent(file.name));
|
||||
|
||||
@@ -57,15 +57,17 @@ export async function handlePartialUpload({
|
||||
if (mime) mimetype = mime;
|
||||
}
|
||||
|
||||
let folder = null;
|
||||
if (options.folder) {
|
||||
const exists = await prisma.folder.findFirst({
|
||||
folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: options.folder,
|
||||
userId: req.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exists) throw 'Folder does not exist';
|
||||
if (!folder) throw 'Folder does not exist';
|
||||
|
||||
if (!folder.allowUploads && folder.userId !== req.user?.id) throw 'Folder is not open';
|
||||
}
|
||||
|
||||
const tempFile = join(
|
||||
@@ -82,7 +84,7 @@ export async function handlePartialUpload({
|
||||
type: mimetype,
|
||||
User: {
|
||||
connect: {
|
||||
id: req.user.id,
|
||||
id: req.user ? req.user.id : options.folder ? folder?.userId : undefined,
|
||||
},
|
||||
},
|
||||
...(options.password && { password: await hashPassword(options.password) }),
|
||||
@@ -98,7 +100,7 @@ export async function handlePartialUpload({
|
||||
new Worker('./build/offload/partial.js', {
|
||||
workerData: {
|
||||
user: {
|
||||
id: req.user.id,
|
||||
id: req.user ? req.user.id : options.folder ? folder?.userId : undefined,
|
||||
},
|
||||
file: {
|
||||
id: fileUpload.id,
|
||||
|
||||
@@ -86,15 +86,17 @@ export async function handleFile({
|
||||
}
|
||||
}
|
||||
|
||||
let folder = null;
|
||||
if (options.folder) {
|
||||
const exists = await prisma.folder.findFirst({
|
||||
folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: options.folder,
|
||||
userId: req.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exists) throw 'Folder does not exist';
|
||||
if (!folder) throw 'Folder does not exist';
|
||||
|
||||
if (!folder.allowUploads && folder.userId !== req.user?.id) throw 'Folder is not open';
|
||||
}
|
||||
|
||||
let compressed = false;
|
||||
@@ -123,7 +125,7 @@ export async function handleFile({
|
||||
type: compressed ? 'image/jpeg' : mimetype,
|
||||
User: {
|
||||
connect: {
|
||||
id: req.user.id,
|
||||
id: req.user ? req.user.id : options.folder ? folder?.userId : undefined,
|
||||
},
|
||||
},
|
||||
...(options.maxViews && { maxViews: options.maxViews }),
|
||||
@@ -150,10 +152,19 @@ export async function handleFile({
|
||||
...(compressed && { compressed: true }),
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} uploaded ${fileUpload.name}`, { size: bytes(fileUpload.size) });
|
||||
logger.info(`${req.user ? req.user.username : '[anonymous folder upload]'} uploaded ${fileUpload.name}`, {
|
||||
size: bytes(fileUpload.size),
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
await onUpload({
|
||||
user: req.user,
|
||||
user: req.user ?? {
|
||||
id: 'anonymous',
|
||||
username: 'anonymous',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: 'USER',
|
||||
},
|
||||
file: fileUpload,
|
||||
link: {
|
||||
raw: `${domain}/raw/${encodeURIComponent(fileUpload.name)}`,
|
||||
|
||||
@@ -5,11 +5,13 @@ export type Folder = PrismaFolder & {
|
||||
files?: File[];
|
||||
};
|
||||
|
||||
export function cleanFolder(folder: Folder, stringifyDates = false) {
|
||||
export function cleanFolder(folder: Partial<Folder>, stringifyDates = false) {
|
||||
if (folder.files) cleanFiles(folder.files, stringifyDates);
|
||||
|
||||
(folder as any).createdAt = stringifyDates ? folder.createdAt.toISOString() : folder.createdAt;
|
||||
(folder as any).updatedAt = stringifyDates ? folder.updatedAt.toISOString() : folder.updatedAt;
|
||||
if (folder.createdAt)
|
||||
(folder as any).createdAt = stringifyDates ? folder.createdAt.toISOString() : folder.createdAt;
|
||||
if (folder.updatedAt)
|
||||
(folder as any).updatedAt = stringifyDates ? folder.updatedAt.toISOString() : folder.updatedAt;
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
54
src/pages/folder/[id]/upload.tsx
Normal file
54
src/pages/folder/[id]/upload.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import ConfigProvider from '@/components/ConfigProvider';
|
||||
import UploadFile from '@/components/pages/upload/File';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { Folder, cleanFolder } from '@/lib/db/models/folder';
|
||||
import { withSafeConfig } from '@/lib/middleware/next/withSafeConfig';
|
||||
import { Container } from '@mantine/core';
|
||||
import { InferGetServerSidePropsType } from 'next';
|
||||
import Head from 'next/head';
|
||||
|
||||
export default function ViewFolderId({
|
||||
folder,
|
||||
config,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
if (!folder) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{`${config.website.title ?? 'Zipline'} – Upload to ${folder.name}`}</title>
|
||||
</Head>
|
||||
|
||||
<Container mt='lg'>
|
||||
<ConfigProvider config={config}>
|
||||
<UploadFile title={`Upload files to ${folder.name}`} folder={folder.id} />
|
||||
</ConfigProvider>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = withSafeConfig<{
|
||||
folder?: Partial<Folder>;
|
||||
}>(async (ctx) => {
|
||||
const { id } = ctx.query;
|
||||
if (!id) return { notFound: true };
|
||||
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: id as string,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
allowUploads: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) return { notFound: true };
|
||||
if (!folder.allowUploads) return { notFound: true };
|
||||
|
||||
return {
|
||||
folder: cleanFolder(folder, true),
|
||||
};
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { User, userSelect } from '@/lib/db/models/user';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { FastifyRequest } from 'fastify/types/request';
|
||||
import { getSession } from '../session';
|
||||
import { parseCookie } from 'next/dist/compiled/@edge-runtime/cookies';
|
||||
|
||||
declare module 'fastify' {
|
||||
export interface FastifyRequest {
|
||||
@@ -39,6 +40,16 @@ export function parseUserToken(
|
||||
}
|
||||
|
||||
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
|
||||
const cookies = parseCookie(req.headers.cookie ?? '');
|
||||
|
||||
// conditions met to allow anonymous folder uploads but later handled in the upload route
|
||||
const anonFolderUpload =
|
||||
req.headers['x-zipline-folder'] &&
|
||||
req.url.toLowerCase().trim() === '/api/upload' &&
|
||||
!req.headers.authorization &&
|
||||
!cookies.has('zipline_session');
|
||||
if (anonFolderUpload) return;
|
||||
|
||||
const authorization = req.headers.authorization;
|
||||
|
||||
if (authorization) {
|
||||
|
||||
@@ -40,6 +40,16 @@ export default fastifyPlugin(
|
||||
const options = parseHeaders(req.headers, config.files);
|
||||
if (options.header) return res.badRequest('bad options, receieved: ' + JSON.stringify(options));
|
||||
|
||||
if (options.folder) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: options.folder,
|
||||
},
|
||||
});
|
||||
if (!folder) return res.badRequest('folder not found');
|
||||
if (!req.user && !folder.allowUploads) return res.forbidden('folder is not open');
|
||||
}
|
||||
|
||||
const filesIterable = req.files();
|
||||
const files: MultipartFileBuffer[] = [];
|
||||
|
||||
@@ -48,7 +58,7 @@ export default fastifyPlugin(
|
||||
files.push(<MultipartFileBuffer>file);
|
||||
}
|
||||
|
||||
if (req.user.quota) {
|
||||
if (req.user?.quota) {
|
||||
const totalFileSize = options.partial
|
||||
? options.partial.contentLength
|
||||
: files.reduce((acc, x) => acc + x.file.bytesRead, 0);
|
||||
|
||||
@@ -15,6 +15,7 @@ type Body = {
|
||||
id?: string;
|
||||
isPublic?: boolean;
|
||||
name?: string;
|
||||
allowUploads?: boolean;
|
||||
|
||||
delete?: 'file' | 'folder';
|
||||
};
|
||||
@@ -100,7 +101,7 @@ export default fastifyPlugin(
|
||||
|
||||
return res.send(cleanFolder(nFolder));
|
||||
} else if (req.method === 'PATCH') {
|
||||
const { isPublic, name } = req.body;
|
||||
const { isPublic, name, allowUploads } = req.body;
|
||||
|
||||
const nFolder = await prisma.folder.update({
|
||||
where: {
|
||||
@@ -109,6 +110,7 @@ export default fastifyPlugin(
|
||||
data: {
|
||||
...(isPublic !== undefined && { public: isPublic }),
|
||||
...(name && { name }),
|
||||
...(allowUploads !== undefined && { allowUploads }),
|
||||
},
|
||||
include: {
|
||||
files: {
|
||||
@@ -123,6 +125,8 @@ export default fastifyPlugin(
|
||||
logger.info('folder updated', {
|
||||
folder: folder.id,
|
||||
isPublic,
|
||||
name,
|
||||
allowUploads,
|
||||
});
|
||||
|
||||
return res.send(cleanFolder(nFolder));
|
||||
|
||||
Reference in New Issue
Block a user