Compare commits

...

11 Commits

Author SHA1 Message Date
diced
69dfad201b feat: reorder/disable/enable table fields in file table 2025-10-12 21:43:50 -07:00
diced
ee1681497e feat: allow any env to be read from a file 2025-10-12 21:43:34 -07:00
diced
2f19140085 feat: add file name in upload response 2025-10-03 21:01:18 -07:00
diced
c9d492f9d2 feat: trust proxies option (#879) 2025-10-03 20:55:35 -07:00
diced
a7a23f3fd9 chore: downgrade aws sdks (#888)
newer AWS sdks introduce dumb AWS specific stuff that break
interoperability with other services.
2025-09-19 20:26:20 -07:00
diced
36ffb669b2 fix: accidental force push lmaoo (#886)
PR: #886
2025-09-18 12:41:22 -07:00
diced
f0ee4cdab3 fix: allow any host on dev 2025-09-18 12:31:59 -07:00
diced
ac41dab2b2 fix: title not updating on first-load 2025-09-09 16:19:54 -07:00
diced
26661f7a83 fix: encode id for view route 2025-09-09 16:06:27 -07:00
diced
01a73df7f3 fix: say "try again" for invites when ratelimited 2025-09-08 23:08:29 -07:00
diced
6b1304f37b fix: #885 2025-09-08 23:06:27 -07:00
21 changed files with 1200 additions and 786 deletions

View File

@@ -21,8 +21,11 @@
"docker:compose:dev:logs": "docker-compose --file docker-compose.dev.yml logs -f"
},
"dependencies": {
"@aws-sdk/client-s3": "3.879.0",
"@aws-sdk/lib-storage": "3.879.0",
"@aws-sdk/client-s3": "3.726.1",
"@aws-sdk/lib-storage": "3.726.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@fastify/multipart": "^9.0.3",

1522
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."Zipline" ADD COLUMN "coreTrustProxy" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -20,6 +20,7 @@ model Zipline {
coreReturnHttpsUrls Boolean @default(false)
coreDefaultDomain String?
coreTempDirectory String // default join(tmpdir(), 'zipline')
coreTrustProxy Boolean @default(false)
chunksEnabled Boolean @default(true)
chunksMax String @default("95mb")

View File

@@ -1,5 +1,6 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useTitle } from '@/lib/hooks/useTitle';
import {
Button,
Center,
@@ -18,10 +19,9 @@ import { useForm } from '@mantine/form';
import { notifications, showNotification } from '@mantine/notifications';
import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { Link, redirect, useLocation, useNavigate } from 'react-router-dom';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import useSWR, { mutate } from 'swr';
import GenericError from '../../error/GenericError';
import { useTitle } from '@/lib/hooks/useTitle';
export function Component() {
useTitle('Register');
@@ -73,7 +73,7 @@ export function Component() {
(async () => {
const res = await fetch('/api/user');
if (res.ok) {
redirect('/dashboard');
navigate('/dashboard');
} else {
setLoading(false);
}
@@ -83,7 +83,7 @@ export function Component() {
useEffect(() => {
if (!config) return;
if (!config?.features.userRegistration) {
if (!config?.features.userRegistration && !code) {
navigate('/auth/login');
}
}, [code, config]);
@@ -122,7 +122,7 @@ export function Component() {
});
mutate('/api/user');
redirect('/dashboard');
navigate('/dashboard');
}
};
@@ -142,7 +142,7 @@ export function Component() {
if (inviteError) {
showNotification({
id: 'invalid-invite',
message: 'Invalid or expired invite.',
message: 'Invalid or expired invite. Please try again later.',
color: 'red',
});

View File

@@ -25,7 +25,7 @@ import { createRoutes } from './routes';
export const getFile = async (id: string) =>
prisma.file.findFirst({
where: { name: id as string },
where: { name: decodeURIComponent(id) },
select: {
...fileSelect,
password: true,

View File

@@ -0,0 +1,99 @@
import { FieldSettings, useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Checkbox, Group, Modal, Paper, Text } from '@mantine/core';
import { IconGripVertical } from '@tabler/icons-react';
import { useShallow } from 'zustand/shallow';
export const NAMES = {
name: 'Name',
originalName: 'Original Name',
tags: 'Tags',
type: 'Type',
size: 'Size',
createdAt: 'Created At',
favorite: 'Favorite',
views: 'Views',
};
function SortableTableField({ item }: { item: FieldSettings }) {
const setVisible = useFileTableSettingsStore((state) => state.setVisible);
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: item.field,
});
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
width: '100%',
};
return (
<Paper withBorder p='xs' ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Group gap='xs'>
<IconGripVertical size='1rem' />
<Checkbox checked={item.visible} onChange={() => setVisible(item.field, !item.visible)} />
<Text>{NAMES[item.field]}</Text>
</Group>
</Paper>
);
}
export default function TableEditModal({ opened, onCLose }: { opened: boolean; onCLose: () => void }) {
const [fields, setIndex, reset] = useFileTableSettingsStore(
useShallow((state) => [state.fields, state.setIndex, state.reset]),
);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor),
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const newIndex = fields.findIndex((item) => item.field === over?.id);
setIndex(active.id as FieldSettings['field'], newIndex);
}
};
return (
<Modal opened={opened} onClose={onCLose} title='Table Options' centered>
<Text mb='md' size='sm' c='dimmed'>
Select and drag fields below to make them appear/disappear/reorder in the file table view.
</Text>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={fields.map((item) => item.field)} strategy={verticalListSortingStrategy}>
{fields.map((item, index) => (
<div
key={index}
style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}
>
<SortableTableField item={item} />
</div>
))}
</SortableContext>
</DndContext>
<Button fullWidth color='red' onClick={() => reset()} variant='light' mt='md'>
Reset to Default
</Button>
</Modal>
);
}

View File

@@ -5,6 +5,8 @@ import { bytes } from '@/lib/bytes';
import { type File } from '@/lib/db/models/file';
import { Folder } from '@/lib/db/models/folder';
import { Tag } from '@/lib/db/models/tag';
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import { useSettingsStore } from '@/lib/store/settings';
import {
ActionIcon,
@@ -34,16 +36,17 @@ import {
IconFile,
IconGridPatternFilled,
IconStar,
IconTableOptions,
IconTrashFilled,
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable';
import { lazy, useEffect, useReducer, useState } from 'react';
import { Link } from 'react-router-dom';
import useSWR from 'swr';
import TableEditModal, { NAMES } from '../TableEditModal';
import { bulkDelete, bulkFavorite } from '../bulk';
import TagPill from '../tags/TagPill';
import { useApiPagination } from '../useApiPagination';
import { useQueryState } from '@/lib/hooks/useQueryState';
const FileModal = lazy(() => import('@/components/file/DashboardFile/FileModal'));
@@ -54,13 +57,6 @@ type ReducerQuery = {
const PER_PAGE_OPTIONS = [10, 20, 50];
const NAMES = {
name: 'Name',
originalName: 'Original name',
type: 'Type',
id: 'ID',
};
function SearchFilter({
setSearchField,
searchQuery,
@@ -88,8 +84,8 @@ function SearchFilter({
return (
<TextInput
label={NAMES[field]}
placeholder={`Search by ${NAMES[field].toLowerCase()}`}
label={NAMES[field as keyof typeof NAMES]}
placeholder={`Search by ${NAMES[field as keyof typeof NAMES].toLowerCase()}`}
value={searchQuery[field]}
onChange={onChange}
size='sm'
@@ -183,6 +179,10 @@ export default function FileTable({ id }: { id?: string }) {
const clipboard = useClipboard();
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
const [tableEditOpen, setTableEditOpen] = useState(false);
const fields = useFileTableSettingsStore((state) => state.fields);
const { data: folders } = useSWR<Extract<Response['/api/user/folders'], Folder[]>>(
'/api/user/folders?noincl=true',
);
@@ -264,6 +264,100 @@ export default function FileTable({ id }: { id?: string }) {
}),
});
const FIELDS = [
{
accessor: 'name',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='name'
/>
),
filtering: searchField === 'name' && searchQuery.name.trim() !== '',
},
{
accessor: 'originalName',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='originalName'
/>
),
filtering: searchField === 'originalName' && searchQuery.originalName.trim() !== '',
},
{
accessor: 'tags',
sortable: false,
width: 200,
render: (file: File) => (
<ScrollArea w={180} onClick={(e) => e.stopPropagation()}>
<Flex gap='sm'>
{file.tags!.map((tag) => (
<TagPill tag={tag} key={tag.id} />
))}
</Flex>
</ScrollArea>
),
filter: (
<TagsFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
),
filtering: searchField === 'tags' && searchQuery.tags.trim() !== '',
},
{
accessor: 'type',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='type'
/>
),
filtering: searchField === 'type' && searchQuery.type.trim() !== '',
},
{ accessor: 'size', sortable: true, render: (file: File) => bytes(file.size) },
{
accessor: 'createdAt',
sortable: true,
render: (file: File) => <RelativeDate date={file.createdAt} />,
},
{
accessor: 'favorite',
sortable: true,
render: (file: File) => (file.favorite ? <Text c='yellow'>Yes</Text> : 'No'),
},
{
accessor: 'views',
sortable: true,
render: (file: File) => file.views,
},
{
accessor: 'id',
hidden: searchField !== 'id' || searchQuery.id.trim() === '',
filtering: searchField === 'id' && searchQuery.id.trim() !== '',
},
];
const visibleFields = fields.filter((f) => f.visible).map((f) => f.field);
const columns = FIELDS.filter((f) => visibleFields.includes(f.accessor as any));
columns.sort((a, b) => {
const aIndex = fields.findIndex((f) => f.field === a.accessor);
const bIndex = fields.findIndex((f) => f.field === b.accessor);
return aIndex - bIndex;
});
useEffect(() => {
if (data && selectedFile) {
const file = data.page.find((x) => x.id === selectedFile.id);
@@ -295,19 +389,32 @@ export default function FileTable({ id }: { id?: string }) {
file={selectedFile}
/>
<TableEditModal opened={tableEditOpen} onCLose={() => setTableEditOpen(false)} />
<Box>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
// lol if it works it works :shrug:
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
<Group>
<Tooltip label='Table Options'>
<ActionIcon
variant='outline'
onClick={() => setTableEditOpen((open) => !open)}
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconTableOptions size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
// lol if it works it works :shrug:
style={{ position: 'relative', top: '-36.4px', left: '221px', margin: 0 }}
>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
<Collapse in={selectedFiles.length > 0}>
<Paper withBorder p='sm' my='sm'>
@@ -417,75 +524,7 @@ export default function FileTable({ id }: { id?: string }) {
minHeight={200}
records={data?.page ?? []}
columns={[
{
accessor: 'name',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='name'
/>
),
filtering: searchField === 'name' && searchQuery.name.trim() !== '',
},
{
accessor: 'tags',
sortable: false,
width: 200,
render: (file) => (
<ScrollArea w={180} onClick={(e) => e.stopPropagation()}>
<Flex gap='sm'>
{file.tags!.map((tag) => (
<TagPill tag={tag} key={tag.id} />
))}
</Flex>
</ScrollArea>
),
filter: (
<TagsFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
),
filtering: searchField === 'tags' && searchQuery.tags.trim() !== '',
},
{
accessor: 'type',
sortable: true,
filter: (
<SearchFilter
setSearchField={setSearchField}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
field='type'
/>
),
filtering: searchField === 'type' && searchQuery.type.trim() !== '',
},
{ accessor: 'size', sortable: true, render: (file) => bytes(file.size) },
{
accessor: 'createdAt',
sortable: true,
render: (file) => <RelativeDate date={file.createdAt} />,
},
{
accessor: 'favorite',
sortable: true,
render: (file) => (file.favorite ? <Text c='yellow'>Yes</Text> : 'No'),
},
{
accessor: 'views',
sortable: true,
render: (file) => file.views,
},
{
accessor: 'id',
hidden: searchField !== 'id' || searchQuery.id.trim() === '',
filtering: searchField === 'id' && searchQuery.id.trim() !== '',
},
...columns,
{
accessor: 'actions',
textAlign: 'right',

View File

@@ -17,11 +17,13 @@ export default function Core({
coreReturnHttpsUrls: boolean;
coreDefaultDomain: string | null | undefined;
coreTempDirectory: string;
coreTrustProxy: boolean;
}>({
initialValues: {
coreReturnHttpsUrls: false,
coreDefaultDomain: '',
coreTempDirectory: '/tmp/zipline',
coreTrustProxy: false,
},
enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false,
@@ -45,6 +47,7 @@ export default function Core({
coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls ?? false,
coreDefaultDomain: data.settings.coreDefaultDomain ?? '',
coreTempDirectory: data.settings.coreTempDirectory ?? '/tmp/zipline',
coreTrustProxy: data.settings.coreTrustProxy ?? false,
});
}, [data]);
@@ -55,14 +58,20 @@ export default function Core({
<Title order={2}>Core</Title>
<form onSubmit={form.onSubmit(onSubmit)}>
<Switch
mt='md'
label='Return HTTPS URLs'
description='Return URLs with HTTPS protocol.'
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
/>
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
<Switch
mt='md'
label='Return HTTPS URLs'
description='Return URLs with HTTPS protocol.'
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
/>
<Switch
label='Trust Proxies'
description='Trust the X-Forwarded-* headers set by proxies. Only enable this if you are behind a trusted proxy (nginx, caddy, etc.). Requires a server restart.'
{...form.getInputProps('coreTrustProxy', { type: 'checkbox' })}
/>
<TextInput
label='Default Domain'
description='The domain to use when generating URLs. This value should not include the protocol.'

View File

@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit';
const DOMAIN_REGEX =
/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$/gim;
/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,30})$/gim;
export default function Domains({
swr: { data, isLoading },

View File

@@ -6,6 +6,7 @@ export const DATABASE_TO_PROP = {
coreReturnHttpsUrls: 'core.returnHttpsUrls',
coreDefaultDomain: 'core.defaultDomain',
coreTempDirectory: 'core.tempDirectory',
coreTrustProxy: 'core.trustProxy',
chunksMax: 'chunks.max',
chunksSize: 'chunks.size',

View File

@@ -1,5 +1,6 @@
import { log } from '@/lib/logger';
import { parse } from './transform';
import { readFileSync } from 'node:fs';
export type EnvType = 'string' | 'string[]' | 'number' | 'boolean' | 'byte' | 'ms' | 'json';
export function env(property: string, env: string | string[], type: EnvType, isDb: boolean = false) {
@@ -32,6 +33,7 @@ export const ENVS = [
env('ssl.cert', 'SSL_CERT', 'string'),
// database stuff
env('core.trustProxy', 'CORE_TRUST_PROXY', 'boolean', true),
env('core.returnHttpsUrls', 'CORE_RETURN_HTTPS_URLS', 'boolean', true),
env('core.defaultDomain', 'CORE_DEFAULT_DOMAIN', 'string', true),
env('core.tempDirectory', 'CORE_TEMP_DIRECTORY', 'string', true),
@@ -177,7 +179,17 @@ export function readEnv(): EnvResult {
env.variable = env.variable.find((v) => process.env[v] !== undefined) || 'DATABASE_URL';
}
const value = process.env[env.variable];
let value = process.env[env.variable];
const valueFileName = process.env[`${env.variable}_FILE`];
if (valueFileName) {
try {
value = readFileSync(valueFileName, 'utf-8').trim();
logger.debug('Using env value from file', { variable: env.variable, file: valueFileName });
} catch (e) {
logger.error(`Failed to read env value from file for ${env.variable}. Skipping...`).error(e as Error);
continue;
}
}
if (value === undefined) continue;

View File

@@ -13,6 +13,7 @@ export const rawConfig: any = {
databaseUrl: undefined,
returnHttpsUrls: undefined,
tempDirectory: undefined,
trustProxy: undefined,
},
chunks: {
max: undefined,

View File

@@ -74,6 +74,7 @@ export const schema = z.object({
.string()
.transform((s) => resolve(s))
.default(join(tmpdir(), 'zipline')),
trustProxy: z.boolean().default(false),
}),
chunks: z.object({
max: z.string().default('95mb'),

View File

@@ -10,5 +10,5 @@ export function useTitle(title?: string) {
useEffect(() => {
if (!data || error || isLoading) return;
document.title = title ? `${data.website.title} ${title}` : data.website.title || 'Zipline';
}, [title, location]);
}, [title, location, data, isLoading]);
}

View File

@@ -0,0 +1,58 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const FIELDS = ['name', 'originalName', 'tags', 'type', 'size', 'createdAt', 'favorite', 'views'] as const;
export const defaultFields: FieldSettings[] = [
{ field: 'name', visible: true },
{ field: 'originalName', visible: false },
{ field: 'tags', visible: true },
{ field: 'type', visible: true },
{ field: 'size', visible: true },
{ field: 'createdAt', visible: true },
{ field: 'favorite', visible: true },
{ field: 'views', visible: true },
];
export type FieldSettings = {
field: (typeof FIELDS)[number];
visible: boolean;
};
export type FileTableSettings = {
fields: FieldSettings[];
setVisible: (field: FieldSettings['field'], visible: boolean) => void;
setIndex: (field: FieldSettings['field'], index: number) => void;
reset: () => void;
};
export const useFileTableSettingsStore = create<FileTableSettings>()(
persist(
(set) => ({
fields: defaultFields,
setVisible: (field, visible) =>
set((state) => ({
fields: state.fields.map((f) => (f.field === field ? { ...f, visible } : f)),
})),
setIndex: (field, index) =>
set((state) => {
const currentIndex = state.fields.findIndex((f) => f.field === field);
if (currentIndex === -1 || index < 0 || index >= state.fields.length) return state;
const newFields = [...state.fields];
const [movedField] = newFields.splice(currentIndex, 1);
newFields.splice(index, 0, movedField);
return { fields: newFields };
}),
reset: () => set({ fields: defaultFields }),
}),
{
name: 'zipline-file-table-settings',
},
),
);

View File

@@ -65,6 +65,13 @@ async function main() {
await mkdir(config.core.tempDirectory, { recursive: true });
logger.debug('creating server', {
port: config.core.port,
hostname: config.core.hostname,
ssl: notNull(config.ssl.key, config.ssl.cert),
trustProxy: config.core.trustProxy,
});
const server = fastify({
https: notNull(config.ssl.key, config.ssl.cert)
? {
@@ -72,6 +79,7 @@ async function main() {
cert: await readFile(config.ssl.cert!, 'utf8'),
}
: null,
trustProxy: config.core.trustProxy,
});
await server.register(fastifyCookie, {

View File

@@ -118,6 +118,7 @@ export default fastifyPlugin(
.nullable()
.refine((value) => !value || /^[a-z0-9-.]+$/.test(value), 'Invalid domain format'),
coreReturnHttpsUrls: z.boolean(),
coreTrustProxy: z.boolean(),
chunksEnabled: z.boolean(),
chunksMax: zBytes,
@@ -321,7 +322,7 @@ export default fastifyPlugin(
z
.string()
.regex(
/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$/gi,
/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,30})$/gi,
'Invalid Domain',
),
),

View File

@@ -41,6 +41,7 @@ export const getExtension = (filename: string, override?: string): string => {
export type ApiUploadResponse = {
files: {
id: string;
name: string;
type: string;
url: string;
pending?: boolean;
@@ -212,6 +213,7 @@ export default fastifyPlugin(
response.files.push({
id: fileUpload.id,
name: fileUpload.name,
type: fileUpload.type,
url: encodeURI(responseUrl),
removedGps: removedGps || undefined,

View File

@@ -7,13 +7,13 @@ import { guess } from '@/lib/mimes';
import { randomCharacters } from '@/lib/random';
import { formatFileName } from '@/lib/uploader/formatFileName';
import { UploadHeaders, UploadOptions, parseHeaders } from '@/lib/uploader/parseHeaders';
import { Prisma } from '@/prisma/client';
import { userMiddleware } from '@/server/middleware/user';
import fastifyPlugin from 'fastify-plugin';
import { readdir, rename, rm } from 'fs/promises';
import { join } from 'path';
import { Worker } from 'worker_threads';
import { ApiUploadResponse, getExtension } from '.';
import { Prisma } from '@/prisma/client';
const logger = log('api').c('upload').c('partial');
@@ -256,6 +256,7 @@ export default fastifyPlugin(
response.files.push({
id: fileUpload.id,
name: fileUpload.name,
type: fileUpload.type,
url: responseUrl,
pending: true,

View File

@@ -14,6 +14,8 @@ export default defineConfig(({ mode }) => {
},
server: {
middlewareMode: true,
// not safe in production, but fine in dev
allowedHosts: true,
},
resolve: {
alias: {