mirror of
https://github.com/diced/zipline.git
synced 2025-05-10 18:05:54 +02:00
Merge branch 'trunk' into feature/file-size
This commit is contained in:
commit
38e30b2525
32 changed files with 1909 additions and 1156 deletions
|
@ -1,5 +1,5 @@
|
|||
# Use the Prisma binaries image as the first stage
|
||||
FROM ghcr.io/diced/prisma-binaries:4.8.x as prisma
|
||||
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
|
||||
|
||||
# Use Alpine Linux as the second stage
|
||||
FROM node:18-alpine3.16 as base
|
||||
|
|
91
package.json
91
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zipline",
|
||||
"version": "3.7.0-rc3",
|
||||
"version": "3.7.0-rc4",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
|
@ -14,81 +14,80 @@
|
|||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node dist",
|
||||
"lint": "next lint",
|
||||
"docker:run": "docker-compose up -d",
|
||||
"docker:up": "docker-compose up",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
||||
"docker:run-dev": "docker-compose --file docker-compose.dev.yml up",
|
||||
"docker:up-dev": "docker-compose --file docker-compose.dev.yml up",
|
||||
"docker:down-dev": "docker-compose --file docker-compose.dev.yml down",
|
||||
"scripts:read-config": "node dist/scripts/read-config",
|
||||
"scripts:import-dir": "node dist/scripts/import-dir",
|
||||
"scripts:list-users": "node dist/scripts/list-users",
|
||||
"scripts:set-user": "node dist/scripts/set-user"
|
||||
"scripts:read-config": "node --enable-source-maps dist/scripts/read-config",
|
||||
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
|
||||
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
|
||||
"scripts:set-user": "node --enable-source-maps dist/scripts/set-user",
|
||||
"scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dicedtomato/mantine-data-grid": "0.0.23",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^5.9.2",
|
||||
"@mantine/dropzone": "^5.9.2",
|
||||
"@mantine/form": "^5.9.2",
|
||||
"@mantine/hooks": "^5.9.2",
|
||||
"@mantine/modals": "^5.9.2",
|
||||
"@mantine/next": "^5.9.2",
|
||||
"@mantine/notifications": "^5.9.2",
|
||||
"@mantine/nprogress": "^5.9.2",
|
||||
"@mantine/prism": "^5.9.2",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"@prisma/internals": "^4.8.1",
|
||||
"@prisma/migrate": "^4.8.1",
|
||||
"@sapphire/shapeshift": "^3.7.1",
|
||||
"@tanstack/react-query": "^4.19.1",
|
||||
"argon2": "^0.30.2",
|
||||
"colorette": "^2.0.19",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/dropzone": "^5.10.5",
|
||||
"@mantine/form": "^5.10.5",
|
||||
"@mantine/hooks": "^5.10.5",
|
||||
"@mantine/modals": "^5.10.5",
|
||||
"@mantine/next": "^5.10.5",
|
||||
"@mantine/notifications": "^5.10.5",
|
||||
"@mantine/prism": "^5.10.5",
|
||||
"@prisma/client": "^4.10.1",
|
||||
"@prisma/internals": "^4.10.1",
|
||||
"@prisma/migrate": "^4.10.1",
|
||||
"@sapphire/shapeshift": "^3.8.1",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"argon2": "^0.30.3",
|
||||
"cookie": "^0.5.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv-expand": "^9.0.0",
|
||||
"exiftool-vendored": "^18.6.0",
|
||||
"fastify": "^4.10.2",
|
||||
"fastify-plugin": "^4.4.0",
|
||||
"dotenv-expand": "^10.0.0",
|
||||
"exiftool-vendored": "^21.2.0",
|
||||
"fastify": "^4.13.0",
|
||||
"fastify-plugin": "^4.5.0",
|
||||
"fflate": "^0.7.4",
|
||||
"find-my-way": "^7.3.1",
|
||||
"find-my-way": "^7.5.0",
|
||||
"katex": "^0.16.4",
|
||||
"mantine-datatable": "^1.8.6",
|
||||
"minio": "^7.0.32",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^13.0.6",
|
||||
"next": "^13.2.1",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^4.8.1",
|
||||
"prisma": "^4.10.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-markdown": "^8.0.4",
|
||||
"recharts": "^2.3.2",
|
||||
"react-markdown": "^8.0.5",
|
||||
"recharts": "^2.4.3",
|
||||
"recoil": "^0.7.6",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.2"
|
||||
"sharp": "^0.31.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/minio": "^7.0.15",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/minio": "^7.0.16",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.12",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/sharp": "^0.31.0",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-config-next": "^13.0.6",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.1",
|
||||
"tsup": "^6.5.0",
|
||||
"typescript": "^4.9.4"
|
||||
"prettier": "^2.8.4",
|
||||
"tsup": "^6.6.3",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
345
src/components/File/FileModal.tsx
Normal file
345
src/components/File/FileModal.tsx
Normal file
|
@ -0,0 +1,345 @@
|
|||
import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import { FileMeta } from '.';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
FileIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from '../icons';
|
||||
import Type from '../Type';
|
||||
|
||||
export default function FileModal({
|
||||
open,
|
||||
setOpen,
|
||||
file,
|
||||
loading,
|
||||
refresh,
|
||||
reducedActions = false,
|
||||
exifEnabled,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
file: any;
|
||||
loading: boolean;
|
||||
refresh: () => void;
|
||||
reducedActions?: boolean;
|
||||
exifEnabled?: boolean;
|
||||
}) {
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const folders = useFolders();
|
||||
|
||||
const [overrideRender, setOverrideRender] = useState(false);
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const handleDelete = async () => {
|
||||
deleteFile.mutate(file.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
setOpen(false);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
favoriteFile.mutate(
|
||||
{ id: file.id, favorite: !file.favorite },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const inFolder = file.folderId;
|
||||
|
||||
const removeFromFolder = async () => {
|
||||
const res = await useFetch('/api/user/folders/' + file.folderId, 'DELETE', {
|
||||
file: Number(file.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Removed from folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderMinusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to remove from folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addToFolder = async (t) => {
|
||||
const res = await useFetch('/api/user/folders/' + t, 'POST', {
|
||||
file: Number(file.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to add to folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (t) => {
|
||||
useFetch('/api/user/folders', 'POST', {
|
||||
name: t,
|
||||
add: [Number(file.id)],
|
||||
}).then((res) => {
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Created & added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { value: t, label: t };
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{file.name}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={file}
|
||||
src={`/r/${encodeURI(file.name)}`}
|
||||
alt={file.name}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
disableMediaPreview={false}
|
||||
overrideRender={overrideRender}
|
||||
setOverrideRender={setOverrideRender}
|
||||
/>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
cols={3}
|
||||
breakpoints={[
|
||||
{ maxWidth: 600, cols: 1 },
|
||||
{ maxWidth: 900, cols: 2 },
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={file.name} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={file.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={file?.views?.toLocaleString()} />
|
||||
{file.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={file?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(file.createdAt))}
|
||||
tooltip={new Date(file?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{file.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(file.expiresAt))}
|
||||
tooltip={new Date(file.expiresAt).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={file.id} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Group position='apart' my='md'>
|
||||
<Group position='left'>
|
||||
{exifEnabled && !reducedActions && (
|
||||
<Tooltip label='View Metadata'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
|
||||
>
|
||||
<InfoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
|
||||
>
|
||||
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
|
||||
<FolderMinusIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label='Add to folder'>
|
||||
<Select
|
||||
onChange={addToFolder}
|
||||
placeholder='Add to folder'
|
||||
data={[
|
||||
...(folders.data ? folders.data : []).map((folder) => ({
|
||||
value: String(folder.id),
|
||||
label: `${folder.id}: ${folder.name}`,
|
||||
})),
|
||||
]}
|
||||
searchable
|
||||
creatable
|
||||
getCreateLabel={(query) => `Create folder "${query}"`}
|
||||
onCreate={createFolder}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{reducedActions ? null : (
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={file.favorite ? 'Unfavorite' : 'Favorite'}>
|
||||
<ActionIcon
|
||||
color={file.favorite ? 'yellow' : 'gray'}
|
||||
variant='filled'
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Copy URL'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Download'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
}
|
90
src/components/File/index.tsx
Normal file
90
src/components/File/index.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Card, Group, LoadingOverlay, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { useState } from 'react';
|
||||
import MutedText from '../MutedText';
|
||||
import Type from '../Type';
|
||||
import FileModal from './FileModal';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||
return other.tooltip ? (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Tooltip label={other.tooltip}>
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({
|
||||
image,
|
||||
disableMediaPreview,
|
||||
exifEnabled,
|
||||
refreshImages,
|
||||
reducedActions = false,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const loading = deleteFile.isLoading || favoriteFile.isLoading;
|
||||
|
||||
const folders = useFolders();
|
||||
|
||||
const refresh = () => {
|
||||
refreshImages();
|
||||
folders.refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
file={image}
|
||||
loading={loading}
|
||||
refresh={refresh}
|
||||
reducedActions={reducedActions}
|
||||
exifEnabled={exifEnabled}
|
||||
/>
|
||||
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Type
|
||||
file={image}
|
||||
sx={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
style={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
onClick={() => setOpen(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,8 +4,10 @@ import {
|
|||
Box,
|
||||
Burger,
|
||||
Button,
|
||||
Group,
|
||||
Header,
|
||||
Image,
|
||||
Input,
|
||||
MediaQuery,
|
||||
Menu,
|
||||
Navbar,
|
||||
|
@ -18,7 +20,7 @@ import {
|
|||
Tooltip,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useClipboard, useMediaQuery } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
@ -214,13 +216,29 @@ export default function Layout({ children, props }) {
|
|||
labels: { confirm: 'Copy', cancel: 'Cancel' },
|
||||
onConfirm: async () => {
|
||||
clipboard.copy(token);
|
||||
|
||||
showNotification({
|
||||
title: 'Token Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: (
|
||||
<Text size='sm'>
|
||||
Zipline is unable to copy to clipboard due to security reasons. However, you can still copy
|
||||
the token manually.
|
||||
<br />
|
||||
<Group position='left' spacing='sm'>
|
||||
<Text>Your token is:</Text>
|
||||
<Input size='sm' onFocus={(e) => e.target.select()} type='text' value={token} />
|
||||
</Group>
|
||||
</Text>
|
||||
),
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Token Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
|
||||
modals.closeAll();
|
||||
},
|
||||
|
@ -323,7 +341,16 @@ export default function Layout({ children, props }) {
|
|||
</MediaQuery>
|
||||
<Title ml='sm'>{title}</Title>
|
||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||
<Menu>
|
||||
<Menu
|
||||
styles={{
|
||||
item: {
|
||||
'@media (max-width: 768px)': {
|
||||
padding: '1rem',
|
||||
width: '80vw',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||
|
@ -395,7 +422,7 @@ export default function Layout({ children, props }) {
|
|||
</>
|
||||
<Menu.Item closeMenuOnClick={false} icon={<PencilIcon />}>
|
||||
<Select
|
||||
size='xs'
|
||||
size={useMediaQuery('(max-width: 768px)') ? 'md' : 'xs'}
|
||||
data={Object.keys(themes).map((t) => ({
|
||||
value: t,
|
||||
label: friendlyThemeName[t],
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import exts from 'lib/exts';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
|
||||
import KaTeX from './render/KaTeX';
|
||||
|
@ -27,6 +27,15 @@ function PlaceholderContent({ text, Icon }) {
|
|||
}
|
||||
|
||||
function Placeholder({ text, Icon, ...props }) {
|
||||
if (props.onClick)
|
||||
return (
|
||||
<UnstyledButton sx={{ height: 200 }} {...props}>
|
||||
<Center sx={{ height: 200 }}>
|
||||
<PlaceholderContent text={text} Icon={Icon} />
|
||||
</Center>
|
||||
</UnstyledButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: 200 }} {...props}>
|
||||
<Center sx={{ height: 200 }}>
|
||||
|
@ -47,10 +56,11 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
const [text, setText] = useState('');
|
||||
const shouldRenderMarkdown = file.name.endsWith('.md');
|
||||
const shouldRenderTex = file.name.endsWith('.tex');
|
||||
const shouldRenderCode: boolean = Object.keys(exts).includes(file.name.split('.').pop());
|
||||
|
||||
const [loading, setLoading] = useState(type === 'text' && popup);
|
||||
|
||||
if (type === 'text' && popup) {
|
||||
if ((type === 'text' || shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && popup) {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/r/' + file.name);
|
||||
|
@ -62,7 +72,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
}, []);
|
||||
}
|
||||
|
||||
const renderRenderAlert = () => {
|
||||
const renderAlert = () => {
|
||||
return (
|
||||
<Alert color='blue' variant='outline' sx={{ width: '100%' }}>
|
||||
You are{props.overrideRender ? ' not ' : ' '}viewing a rendered version of the file
|
||||
|
@ -78,39 +88,53 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
);
|
||||
};
|
||||
|
||||
if ((shouldRenderMarkdown || shouldRenderTex) && !props.overrideRender && popup)
|
||||
if ((shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && !props.overrideRender && popup)
|
||||
return (
|
||||
<>
|
||||
{renderRenderAlert()}
|
||||
{renderAlert()}
|
||||
<Card p='md' my='sm'>
|
||||
{shouldRenderMarkdown && <Markdown code={text} />}
|
||||
{shouldRenderTex && <KaTeX code={text} />}
|
||||
{shouldRenderCode && !(shouldRenderTex || shouldRenderMarkdown) && (
|
||||
<PrismCode code={text} ext={type} />
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
||||
if (media && disableMediaPreview) {
|
||||
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />;
|
||||
return <Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />;
|
||||
}
|
||||
|
||||
if (file.password) {
|
||||
return (
|
||||
<Placeholder
|
||||
Icon={FileIcon}
|
||||
text={`This file is password protected. Click to view file (${file.name})`}
|
||||
onClick={() => window.open(file.url)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return popup ? (
|
||||
media ? (
|
||||
{
|
||||
video: <video width='100%' autoPlay controls {...props} />,
|
||||
video: <video width='100%' autoPlay muted controls {...props} />,
|
||||
image: (
|
||||
<Image
|
||||
placeholder={<PlaceholderContent Icon={FileIcon} text={'Image failed to load...'} />}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
|
||||
audio: <audio autoPlay muted controls {...props} style={{ width: '100%' }} />,
|
||||
text: (
|
||||
<>
|
||||
{loading ? (
|
||||
<LoadingOverlay visible={loading} />
|
||||
) : (
|
||||
<>
|
||||
{(shouldRenderMarkdown || shouldRenderTex) && renderRenderAlert()}
|
||||
{(shouldRenderMarkdown || shouldRenderTex) && renderAlert()}
|
||||
<PrismCode code={text} ext={file.name.split('.').pop()} {...props} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,42 +1,88 @@
|
|||
import { DataGrid, dateFilterFn, stringFilterFn } from '@dicedtomato/mantine-data-grid';
|
||||
import { Title, useMantineTheme, Box } from '@mantine/core';
|
||||
import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon } from 'components/icons';
|
||||
import FileModal from 'components/File/FileModal';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon, FileIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useFiles, useRecent } from 'lib/queries/files';
|
||||
import { usePaginatedFiles, useRecent } from 'lib/queries/files';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import RecentFiles from './RecentFiles';
|
||||
import { StatCards } from './StatCards';
|
||||
|
||||
export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
||||
const user = useRecoilValue(userSelector);
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const images = useFiles();
|
||||
const recent = useRecent('media');
|
||||
const stats = useStats();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const updateImages = () => {
|
||||
images.refetch();
|
||||
// pagination
|
||||
const [, setNumPages] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [numFiles, setNumFiles] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { count } = await useFetch('/api/user/paged?count=true');
|
||||
setNumPages(count);
|
||||
|
||||
const { count: filesCount } = await useFetch('/api/user/files?count=true');
|
||||
setNumFiles(filesCount);
|
||||
})();
|
||||
}, [page]);
|
||||
|
||||
const files = usePaginatedFiles(page);
|
||||
|
||||
// sorting
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'date',
|
||||
direction: 'asc',
|
||||
});
|
||||
const [records, setRecords] = useState(files.data);
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(files.data);
|
||||
}, [files.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
if (sortStatus.direction === 'asc') {
|
||||
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
}
|
||||
|
||||
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
});
|
||||
|
||||
setRecords(sortedRecords);
|
||||
}, [sortStatus]);
|
||||
|
||||
// file modal on click
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
|
||||
const updateFiles = () => {
|
||||
files.refetch();
|
||||
recent.refetch();
|
||||
stats.refetch();
|
||||
};
|
||||
|
||||
const deleteImage = async ({ original }) => {
|
||||
const deleteFile = async (file) => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', {
|
||||
id: original.id,
|
||||
id: file.id,
|
||||
});
|
||||
if (!res.error) {
|
||||
updateImages();
|
||||
updateFiles();
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: `${original.name}`,
|
||||
message: `${file.name}`,
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
|
@ -50,28 +96,47 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
|||
}
|
||||
};
|
||||
|
||||
const copyImage = async ({ original }) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: (
|
||||
<a
|
||||
href={`${window.location.protocol}//${window.location.host}${original.url}`}
|
||||
>{`${window.location.protocol}//${window.location.host}${original.url}`}</a>
|
||||
),
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
const copyFile = async (file) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: (
|
||||
<a
|
||||
href={`${window.location.protocol}//${window.location.host}${file.url}`}
|
||||
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
|
||||
),
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const viewImage = async ({ original }) => {
|
||||
window.open(`${window.location.protocol}//${window.location.host}${original.url}`);
|
||||
const viewFile = async (file) => {
|
||||
window.open(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{selectedFile && (
|
||||
<FileModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
file={selectedFile}
|
||||
loading={files.isLoading}
|
||||
refresh={() => files.refetch()}
|
||||
reducedActions={false}
|
||||
exifEnabled={exifEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Title>Welcome back, {user?.username}</Title>
|
||||
<MutedText size='md'>
|
||||
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files
|
||||
You have <b>{numFiles === 0 ? '...' : numFiles}</b> files
|
||||
</MutedText>
|
||||
|
||||
<StatCards />
|
||||
|
@ -83,74 +148,92 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
|||
<MutedText size='md'>
|
||||
View your gallery <Link href='/dashboard/files'>here</Link>.
|
||||
</MutedText>
|
||||
<DataGrid
|
||||
data={images.data ?? []}
|
||||
loading={images.isLoading}
|
||||
withPagination={true}
|
||||
withColumnResizing={false}
|
||||
withColumnFilters={true}
|
||||
noEllipsis={true}
|
||||
withSorting={true}
|
||||
highlightOnHover={true}
|
||||
CopyIcon={CopyIcon}
|
||||
DeleteIcon={DeleteIcon}
|
||||
EnterIcon={EnterIcon}
|
||||
deleteImage={deleteImage}
|
||||
copyImage={copyImage}
|
||||
viewImage={viewImage}
|
||||
styles={{
|
||||
dataCell: {
|
||||
width: '100%',
|
||||
},
|
||||
td: {
|
||||
':nth-of-child(1)': {
|
||||
minWidth: 170,
|
||||
},
|
||||
':nth-of-child(2)': {
|
||||
minWidth: 100,
|
||||
},
|
||||
},
|
||||
th: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
|
||||
':nth-of-child(1)': {
|
||||
minWidth: 170,
|
||||
padding: theme.spacing.lg,
|
||||
borderTopLeftRadius: theme.radius.sm,
|
||||
},
|
||||
':nth-of-child(2)': {
|
||||
minWidth: 100,
|
||||
padding: theme.spacing.lg,
|
||||
},
|
||||
':nth-of-child(3)': {
|
||||
padding: theme.spacing.lg,
|
||||
},
|
||||
':nth-of-child(4)': {
|
||||
padding: theme.spacing.lg,
|
||||
borderTopRightRadius: theme.radius.sm,
|
||||
},
|
||||
},
|
||||
thead: {
|
||||
backgroundColor: theme.colors.dark[6],
|
||||
},
|
||||
}}
|
||||
empty={<></>}
|
||||
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{ accessor: 'name', sortable: true },
|
||||
{ accessor: 'mimetype', sortable: true },
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
filterFn: stringFilterFn,
|
||||
accessor: 'createdAt',
|
||||
sortable: true,
|
||||
render: (file) => new Date(file.createdAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'mimetype',
|
||||
header: 'Type',
|
||||
filterFn: stringFilterFn,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Date',
|
||||
filterFn: dateFilterFn,
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (file) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='More details'>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setSelectedFile(file);
|
||||
setOpen(true);
|
||||
}}
|
||||
color='blue'
|
||||
>
|
||||
<FileIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Open file in new tab'>
|
||||
<ActionIcon onClick={() => viewFile(file)} color='blue'>
|
||||
<EnterIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<ActionIcon onClick={() => copyFile(file)} color='green'>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon onClick={() => deleteFile(file)} color='red'>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
records={records ?? []}
|
||||
fetching={files.isLoading}
|
||||
loaderBackgroundBlur={5}
|
||||
loaderVariant='dots'
|
||||
minHeight={620}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
recordsPerPage={16}
|
||||
totalRecords={numFiles}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
rowContextMenu={{
|
||||
shadow: 'xl',
|
||||
borderRadius: 'md',
|
||||
items: (file) => [
|
||||
{
|
||||
key: 'view',
|
||||
icon: <EnterIcon />,
|
||||
title: `View ${file.name}`,
|
||||
onClick: () => viewFile(file),
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <CopyIcon />,
|
||||
title: `Copy ${file.name}`,
|
||||
onClick: () => copyFile(file),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <DeleteIcon />,
|
||||
title: `Delete ${file.name}`,
|
||||
onClick: () => deleteFile(file),
|
||||
},
|
||||
],
|
||||
}}
|
||||
onCellClick={({ record: file }) => {
|
||||
setSelectedFile(file);
|
||||
setOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Box, Button, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import File from 'components/File';
|
||||
import { FileIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
@ -14,6 +15,8 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
|||
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));
|
||||
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -92,13 +95,15 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
|||
paddingBottom: 3,
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
<Pagination total={numPages} page={page} onChange={setPage} />
|
||||
<Checkbox
|
||||
label='Show non-media files'
|
||||
checked={checked}
|
||||
onChange={(event) => setChecked(event.currentTarget.checked)}
|
||||
/>
|
||||
{!isMobile && <div></div>}
|
||||
<Pagination total={numPages} page={page} onChange={setPage} withEdges />
|
||||
{!isMobile && (
|
||||
<Checkbox
|
||||
label='Show non-media files'
|
||||
checked={checked}
|
||||
onChange={(event) => setChecked(event.currentTarget.checked)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -165,16 +165,23 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
|||
aria-label='copy link'
|
||||
onClick={() => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: (
|
||||
<>
|
||||
Copied <Link href={`/folder/${folder.id}`}>folder link</Link> to clipboard
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: (
|
||||
<>
|
||||
Copied <Link href={`/folder/${folder.id}`}>folder link</Link> to clipboard
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
|
|
|
@ -158,11 +158,18 @@ export default function Invites() {
|
|||
|
||||
const handleCopy = async (invite) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}/auth/register?code=${invite.code}`);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const updateInvites = async () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Button, Center, Image, Modal, NumberInput, Text, Title } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { CheckIcon, CrossIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
@ -10,6 +11,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
|||
const [disabled, setDisabled] = useState(false);
|
||||
const [code, setCode] = useState(undefined);
|
||||
const [error, setError] = useState('');
|
||||
const form = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -114,28 +116,35 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
|||
</>
|
||||
)}
|
||||
|
||||
<NumberInput
|
||||
placeholder='2FA Code'
|
||||
label='Verify'
|
||||
size='xl'
|
||||
hideControls
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e)}
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size='lg'
|
||||
fullWidth
|
||||
mt='md'
|
||||
rightIcon={<CheckIcon />}
|
||||
onClick={deleteTotp ? disableTotp : verifyCode}
|
||||
<form
|
||||
onSubmit={form.onSubmit(() => {
|
||||
deleteTotp ? disableTotp() : verifyCode();
|
||||
})}
|
||||
>
|
||||
Verify{deleteTotp ? ' and Disable' : ''}
|
||||
</Button>
|
||||
<NumberInput
|
||||
placeholder='2FA Code'
|
||||
label='Verify'
|
||||
size='xl'
|
||||
hideControls
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e)}
|
||||
data-autofocus
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size='lg'
|
||||
fullWidth
|
||||
mt='md'
|
||||
rightIcon={<CheckIcon />}
|
||||
onClick={deleteTotp ? disableTotp : verifyCode}
|
||||
>
|
||||
Verify{deleteTotp ? ' and Disable' : ''}
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { randomId, useInterval } from '@mantine/hooks';
|
||||
import { randomId, useInterval, useMediaQuery } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import {
|
||||
|
@ -40,7 +40,7 @@ import useFetch from 'hooks/useFetch';
|
|||
import { userSelector } from 'lib/recoil/user';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { capitalize } from 'lib/utils/client';
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import ClearStorage from './ClearStorage';
|
||||
import Flameshot from './Flameshot';
|
||||
|
@ -394,7 +394,18 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
/>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button type='submit'>Save User</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
size='lg'
|
||||
my='sm'
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
|
@ -407,7 +418,16 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
: 'You do not have two factor authentication enabled.'}
|
||||
</MutedText>
|
||||
|
||||
<Button size='lg' my='sm' onClick={() => setTotpOpen(true)}>
|
||||
<Button
|
||||
size='lg'
|
||||
my='sm'
|
||||
onClick={() => setTotpOpen(true)}
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{totpEnabled ? 'Disable' : 'Enable'} Two Factor Authentication
|
||||
</Button>
|
||||
|
||||
|
@ -483,7 +503,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
</Button>
|
||||
</Card>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Group position='right' my='md' grow={useMediaQuery('(max-width: 768px)')}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
|
@ -497,12 +517,12 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box mb='md'>
|
||||
<Box my='md'>
|
||||
<Title>Manage Data</Title>
|
||||
<MutedText size='md'>Delete, or export your data into a zip file.</MutedText>
|
||||
</Box>
|
||||
|
||||
<Group>
|
||||
<Group my='md' grow={useMediaQuery('(max-width: 768px)')}>
|
||||
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>
|
||||
Delete All Data
|
||||
</Button>
|
||||
|
@ -545,7 +565,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
{user.administrator && (
|
||||
<Box mt='md'>
|
||||
<Title>Server</Title>
|
||||
<Group>
|
||||
<Group my='md' grow={useMediaQuery('(max-width: 768px)')}>
|
||||
<Button size='md' onClick={forceUpdateStats} color='red' rightIcon={<RefreshIcon />}>
|
||||
Force Update Stats
|
||||
</Button>
|
||||
|
@ -558,10 +578,28 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
|||
|
||||
<Title my='md'>Uploaders</Title>
|
||||
<Group>
|
||||
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>
|
||||
<Button
|
||||
size='xl'
|
||||
onClick={() => setShareXOpen(true)}
|
||||
rightIcon={<ShareXIcon />}
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Generate ShareX Config
|
||||
</Button>
|
||||
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>
|
||||
<Button
|
||||
size='xl'
|
||||
onClick={() => setFlameshotOpen(true)}
|
||||
rightIcon={<FlameshotIcon />}
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Generate Flameshot Script
|
||||
</Button>
|
||||
</Group>
|
||||
|
|
|
@ -27,11 +27,18 @@ export default function MetadataView({ fileId }) {
|
|||
|
||||
const copy = (value) => {
|
||||
clipboard.copy(value);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: value,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: value,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const searchValue = (value) => {
|
||||
|
|
|
@ -128,6 +128,12 @@ export default function File({ chunks: chunks_config }) {
|
|||
setTimeout(() => setProgress(0), 1000);
|
||||
|
||||
clipboard.copy(json.files[0]);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
|
||||
ready = true;
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import { Button, Table, Title } from '@mantine/core';
|
||||
import { ActionIcon, Box, Button, Group, Stack, Table, Title, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CopyIcon } from 'components/icons';
|
||||
import { CopyIcon, LinkIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
|
||||
export default function showFilesModal(clipboard, modals, files: string[]) {
|
||||
const open = (idx: number) => window.open(files[idx], '_blank');
|
||||
const copy = (idx: number) => {
|
||||
clipboard.copy(files[idx]);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: <Link href={files[idx]}>{files[idx]}</Link>,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: <Link href={files[idx]}>{files[idx]}</Link>,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
modals.openModal({
|
||||
|
@ -19,25 +26,27 @@ export default function showFilesModal(clipboard, modals, files: string[]) {
|
|||
size: 'auto',
|
||||
children: (
|
||||
<Table withBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
|
||||
<tbody>
|
||||
<Stack>
|
||||
{files.map((file, idx) => (
|
||||
<tr key={file}>
|
||||
<td>
|
||||
<Group key={idx} position='apart'>
|
||||
<Group position='left'>
|
||||
<Link href={file}>{file}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Button.Group>
|
||||
<Button variant='outline' onClick={() => copy(idx)}>
|
||||
Copy
|
||||
</Button>
|
||||
<Button variant='outline' onClick={() => open(idx)}>
|
||||
Open
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</td>
|
||||
</tr>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<Tooltip label='Open link in a new tab'>
|
||||
<ActionIcon onClick={() => open(idx)} variant='filled' color='primary'>
|
||||
<LinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Copy link to clipboard'>
|
||||
<ActionIcon onClick={() => copy(idx)} variant='filled' color='primary'>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
))}
|
||||
</tbody>
|
||||
</Stack>
|
||||
</Table>
|
||||
),
|
||||
});
|
||||
|
|
|
@ -14,11 +14,18 @@ export default function URLCard({ url }: { url: URLResponse }) {
|
|||
|
||||
const copyURL = (u) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteURL = async (u) => {
|
||||
|
|
|
@ -250,10 +250,28 @@ export default function validate(config): Config {
|
|||
}
|
||||
}
|
||||
|
||||
const reserved = ['/view', '/dashboard', '/code', '/folder', '/api', '/auth'];
|
||||
if (reserved.some((r) => validated.uploader.route.startsWith(r))) {
|
||||
throw {
|
||||
errors: [`The uploader route cannot be ${validated.uploader.route}, this is a reserved route.`],
|
||||
show: true,
|
||||
};
|
||||
} else if (reserved.some((r) => validated.urls.route.startsWith(r))) {
|
||||
throw {
|
||||
errors: [`The urls route cannot be ${validated.urls.route}, this is a reserved route.`],
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
|
||||
return validated as unknown as Config;
|
||||
} catch (e) {
|
||||
if (process.env.ZIPLINE_DOCKER_BUILD) return null;
|
||||
|
||||
if (e.show) {
|
||||
Logger.get('config').error('Config is invalid, see below:').error(e.errors.join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.debug(`config error: ${inspect(e, { depth: Infinity })}`);
|
||||
|
||||
e.stack = '';
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { blueBright, cyan, red, yellow } from 'colorette';
|
||||
const COLORS = {
|
||||
blueBright: (str: string) => `\x1b[34m${str}\x1b[0m`,
|
||||
cyan: (str: string) => `\x1b[36m${str}\x1b[0m`,
|
||||
red: (str: string) => `\x1b[31m${str}\x1b[0m`,
|
||||
yellow: (str: string) => `\x1b[33m${str}\x1b[0m`,
|
||||
gray: (str: string) => `\x1b[90m${str}\x1b[0m`,
|
||||
};
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export enum LoggerLevel {
|
||||
|
@ -10,6 +17,10 @@ export enum LoggerLevel {
|
|||
export default class Logger {
|
||||
public name: string;
|
||||
|
||||
static filters(): string[] {
|
||||
return (process.env.LOGGER_FILTERS ?? '').split(',').filter((x) => x !== '');
|
||||
}
|
||||
|
||||
static get(klass: any) {
|
||||
if (typeof klass !== 'function') if (typeof klass !== 'string') throw new Error('not string/function');
|
||||
|
||||
|
@ -26,13 +37,24 @@ export default class Logger {
|
|||
return new Logger(`${this.name}::${name}`);
|
||||
}
|
||||
|
||||
show(): boolean {
|
||||
const filters = Logger.filters();
|
||||
if (!filters.length) return true;
|
||||
|
||||
return filters.includes(this.name);
|
||||
}
|
||||
|
||||
info(...args: any[]): this {
|
||||
if (!this.show()) return this;
|
||||
|
||||
process.stdout.write(this.formatMessage(LoggerLevel.INFO, this.name, args.join(' ')));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
error(...args: any[]): this {
|
||||
if (!this.show()) return this;
|
||||
|
||||
process.stdout.write(
|
||||
this.formatMessage(LoggerLevel.ERROR, this.name, args.map((error) => error.stack ?? error).join(' '))
|
||||
);
|
||||
|
@ -41,7 +63,8 @@ export default class Logger {
|
|||
}
|
||||
|
||||
debug(...args: any[]): this {
|
||||
if (!process.env.DEBUG) return;
|
||||
if (!process.env.DEBUG) return this;
|
||||
if (!this.show()) return this;
|
||||
|
||||
process.stdout.write(this.formatMessage(LoggerLevel.DEBUG, this.name, args.join(' ')));
|
||||
|
||||
|
@ -50,17 +73,17 @@ export default class Logger {
|
|||
|
||||
formatMessage(level: LoggerLevel, name: string, message: string) {
|
||||
const time = dayjs().format('YYYY-MM-DD hh:mm:ss,SSS A');
|
||||
return `${time} ${this.formatLevel(level)} [${blueBright(name)}] ${message}\n`;
|
||||
return `${COLORS.gray(time)} ${this.formatLevel(level)} [${COLORS.blueBright(name)}] ${message}\n`;
|
||||
}
|
||||
|
||||
formatLevel(level: LoggerLevel) {
|
||||
switch (level) {
|
||||
case LoggerLevel.INFO:
|
||||
return cyan('info ');
|
||||
return COLORS.cyan('info ');
|
||||
case LoggerLevel.ERROR:
|
||||
return red('error');
|
||||
return COLORS.red('error');
|
||||
case LoggerLevel.DEBUG:
|
||||
return yellow('debug');
|
||||
return COLORS.yellow('debug');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,6 @@ export async function removeGPSData(image: File): Promise<void> {
|
|||
GPSLongitude: null,
|
||||
GPSLongitudeRef: null,
|
||||
GPSMapDatum: null,
|
||||
GPSMeasureMode: null,
|
||||
GPSPosition: null,
|
||||
GPSProcessingMethod: null,
|
||||
GPSSatellites: null,
|
||||
|
|
|
@ -191,8 +191,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
}
|
||||
|
||||
if (zconfig.exif.enabled && zconfig.exif.remove_gps && mimetype.startsWith('image/')) {
|
||||
await removeGPSData(file);
|
||||
response.removed_gps = true;
|
||||
try {
|
||||
await removeGPSData(file);
|
||||
response.removed_gps = true;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to remove GPS data from ${file.name} (${file.id}) - ${e.message}`);
|
||||
|
||||
response.removed_gps = false;
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(response);
|
||||
|
@ -330,8 +336,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
}
|
||||
|
||||
if (zconfig.exif.enabled && zconfig.exif.remove_gps && fileUpload.mimetype.startsWith('image/')) {
|
||||
await removeGPSData(fileUpload);
|
||||
response.removed_gps = true;
|
||||
try {
|
||||
await removeGPSData(fileUpload);
|
||||
response.removed_gps = true;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to remove GPS data from ${fileUpload.name} (${fileUpload.id}) - ${e.message}`);
|
||||
|
||||
response.removed_gps = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
|
||||
logger.info(`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id})`);
|
||||
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
return res.json(file);
|
||||
}
|
||||
} else if (req.method === 'PATCH') {
|
||||
|
@ -57,9 +59,20 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
},
|
||||
});
|
||||
|
||||
delete image.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
return res.json(image);
|
||||
} else {
|
||||
if (req.query.count) {
|
||||
const count = await prisma.file.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
favorite: !!req.query.favorite,
|
||||
},
|
||||
});
|
||||
|
||||
return res.json({ count });
|
||||
}
|
||||
let files: {
|
||||
favorite: boolean;
|
||||
createdAt: Date;
|
||||
|
|
|
@ -89,7 +89,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (req.query.files) {
|
||||
for (let i = 0; i !== folder.files.length; ++i) {
|
||||
const file = folder.files[i];
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
|
@ -123,7 +124,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (req.query.files) {
|
||||
for (let i = 0; i !== folder.files.length; ++i) {
|
||||
const file = folder.files[i];
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
|
@ -217,7 +219,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (req.query.files) {
|
||||
for (let i = 0; i !== folder.files.length; ++i) {
|
||||
const file = folder.files[i];
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
|
@ -232,7 +235,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
if (req.query.files) {
|
||||
for (let i = 0; i !== folder.files.length; ++i) {
|
||||
const file = folder.files[i];
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
|
|
|
@ -81,7 +81,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
const folder = folders[i];
|
||||
for (let j = 0; j !== folders[i].files.length; ++j) {
|
||||
const file = folder.files[j];
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
|
|
|
@ -59,6 +59,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
views: number;
|
||||
folderId: number;
|
||||
size: number;
|
||||
password: string | boolean;
|
||||
}[] = await prisma.file.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
|
@ -75,13 +76,17 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
maxViews: true,
|
||||
folderId: true,
|
||||
size: true,
|
||||
password: true,
|
||||
},
|
||||
skip: page ? (Number(page) - 1) * pageCount : undefined,
|
||||
take: page ? pageCount : undefined,
|
||||
});
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
(files[i] as unknown as { url: string }).url = formatRootUrl(config.uploader.route, files[i].name);
|
||||
const file = files[i];
|
||||
if (file.password) file.password = true;
|
||||
|
||||
(file as unknown as { url: string }).url = formatRootUrl(config.uploader.route, file.name);
|
||||
}
|
||||
|
||||
return res.json(files);
|
||||
|
|
|
@ -98,28 +98,24 @@ export default function Login({ title, user_registration, oauth_registration, oa
|
|||
title={<Title order={3}>Two-Factor Authentication Required</Title>}
|
||||
size='lg'
|
||||
>
|
||||
<NumberInput
|
||||
placeholder='2FA Code'
|
||||
label='Verify'
|
||||
size='xl'
|
||||
hideControls
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e)}
|
||||
error={error}
|
||||
/>
|
||||
<form onSubmit={form.onSubmit(() => onSubmit(form.values))}>
|
||||
<NumberInput
|
||||
placeholder='2FA Code'
|
||||
label='Verify'
|
||||
size='xl'
|
||||
hideControls
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e)}
|
||||
data-autofocus
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size='lg'
|
||||
fullWidth
|
||||
mt='md'
|
||||
rightIcon={<CheckIcon />}
|
||||
onClick={() => onSubmit(form.values)}
|
||||
>
|
||||
Verify & Login
|
||||
</Button>
|
||||
<Button disabled={disabled} size='lg' fullWidth mt='md' rightIcon={<CheckIcon />} type='submit'>
|
||||
Verify & Login
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
<Center sx={{ height: '100vh' }}>
|
||||
<div>
|
||||
|
|
|
@ -35,8 +35,8 @@ export default function Folder({ title, folder }: Props) {
|
|||
<title>{full_title}</title>
|
||||
</Head>
|
||||
<Container size='lg'>
|
||||
<Title align='center' my='lg'>
|
||||
Viewing folder: {folder.name}
|
||||
<Title size={50} align='center' my='lg'>
|
||||
{folder.name}
|
||||
</Title>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
|
@ -80,6 +80,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||
id: true,
|
||||
views: true,
|
||||
createdAt: true,
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
|
@ -101,6 +102,9 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||
folder.files[j].name
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
if (folder.files[j].password) folder.files[j].password = true;
|
||||
|
||||
(folder.files[j].createdAt as unknown) = folder.files[j].createdAt.toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -197,7 +197,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
};
|
||||
else if (prismRender && file.password) {
|
||||
const pass = file.password ? true : false;
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
return {
|
||||
props: {
|
||||
image: file,
|
||||
|
@ -214,7 +215,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
const data = await datasource.get(file.name);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
delete file.password;
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
@ -223,14 +225,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
},
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
if (file.password) file.password = true;
|
||||
|
||||
const pass = file.password ? true : false;
|
||||
delete file.password;
|
||||
return {
|
||||
props: {
|
||||
file,
|
||||
user,
|
||||
pass,
|
||||
pass: file.password ? true : false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
64
src/scripts/clear-zero-byte.ts
Normal file
64
src/scripts/clear-zero-byte.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { readdir, readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import config from 'lib/config';
|
||||
import datasource from 'lib/datasource';
|
||||
import { guess } from 'lib/mimes';
|
||||
import { migrations } from 'server/util';
|
||||
|
||||
async function main() {
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
await migrations();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const files = await prisma.file.findMany();
|
||||
|
||||
const toDelete = [];
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
const size = await datasource.size(file.name);
|
||||
if (size === 0) {
|
||||
toDelete.push(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (toDelete.length === 0) {
|
||||
console.log('No files to delete.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stdout.write(`Found ${toDelete.length} files to delete. Continue? (y/N) `);
|
||||
const answer: Buffer = await new Promise((resolve) => {
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (text) => {
|
||||
resolve(text);
|
||||
});
|
||||
});
|
||||
|
||||
if (answer.toString().trim().toLowerCase() !== 'y') {
|
||||
console.log('Aborting.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { count } = await prisma.file.deleteMany({
|
||||
where: {
|
||||
name: {
|
||||
in: toDelete,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(`Deleted ${count} files from the database.`);
|
||||
|
||||
for (let i = 0; i !== toDelete.length; ++i) {
|
||||
await datasource.delete(toDelete[i]);
|
||||
}
|
||||
|
||||
console.log(`Deleted ${toDelete.length} files from the storage.`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,4 +1,5 @@
|
|||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import exts from 'lib/exts';
|
||||
|
||||
export default async function uploadsRoute(this: FastifyInstance, req: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = req.params as { id: string };
|
||||
|
@ -16,7 +17,9 @@ export default async function uploadsRoute(this: FastifyInstance, req: FastifyRe
|
|||
const failed = await reply.preFile(image);
|
||||
if (failed) return reply.notFound();
|
||||
|
||||
if (image.password || image.embed || image.mimetype.startsWith('text/'))
|
||||
const ext = image.name.split('.').pop();
|
||||
|
||||
if (image.password || image.embed || image.mimetype.startsWith('text/') || Object.keys(exts).includes(ext))
|
||||
return reply.redirect(`/view/${image.name}`);
|
||||
else return reply.dbFile(image);
|
||||
}
|
||||
|
|
|
@ -27,12 +27,8 @@
|
|||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"zip-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/**/*.ts",
|
||||
"**/**/*.tsx",
|
||||
"prisma/seed.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", ".yarn", ".next"]
|
||||
}
|
||||
|
|
|
@ -34,4 +34,9 @@ export default defineConfig([
|
|||
outDir: 'dist/scripts',
|
||||
...opts,
|
||||
},
|
||||
{
|
||||
entryPoints: ['src/scripts/clear-zero-byte.ts'],
|
||||
outDir: 'dist/scripts',
|
||||
...opts,
|
||||
},
|
||||
]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue