mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
Compare commits
11 Commits
v4.3.1
...
69dfad201b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69dfad201b | ||
|
|
ee1681497e | ||
|
|
2f19140085 | ||
|
|
c9d492f9d2 | ||
|
|
a7a23f3fd9 | ||
|
|
36ffb669b2 | ||
|
|
f0ee4cdab3 | ||
|
|
ac41dab2b2 | ||
|
|
26661f7a83 | ||
|
|
01a73df7f3 | ||
|
|
6b1304f37b |
@@ -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
1522
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "coreTrustProxy" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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")
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
99
src/components/pages/files/TableEditModal.tsx
Normal file
99
src/components/pages/files/TableEditModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const rawConfig: any = {
|
||||
databaseUrl: undefined,
|
||||
returnHttpsUrls: undefined,
|
||||
tempDirectory: undefined,
|
||||
trustProxy: undefined,
|
||||
},
|
||||
chunks: {
|
||||
max: undefined,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
58
src/lib/store/fileTableSettings.ts
Executable file
58
src/lib/store/fileTableSettings.ts
Executable 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',
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -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, {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,8 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
// not safe in production, but fine in dev
|
||||
allowedHosts: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user