feat: expiring images
This commit is contained in:
parent
cdcb31130b
commit
f3a23a528b
10 changed files with 162 additions and 7 deletions
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "expires_at" TIMESTAMP(3);
|
|
@ -36,6 +36,7 @@ model Image {
|
|||
file String
|
||||
mimetype String @default("image/png")
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime?
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
|
|
|
@ -4,8 +4,9 @@ import { showNotification } from '@mantine/notifications';
|
|||
import useFetch from 'hooks/useFetch';
|
||||
import { useState } from 'react';
|
||||
import Type from './Type';
|
||||
import { CalendarIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
||||
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
||||
import MutedText from './MutedText';
|
||||
import { relativeTime } from 'lib/clientUtils';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle }) {
|
||||
return (
|
||||
|
@ -22,7 +23,6 @@ export function FileMeta({ Icon, title, subtitle }) {
|
|||
export default function File({ image, updateImages }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const clipboard = useClipboard();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
|
@ -88,6 +88,7 @@ export default function File({ image, updateImages }) {
|
|||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} />
|
||||
{image.expires_at && <FileMeta Icon={ClockIcon} title='Expires' subtitle={relativeTime(new Date(image.expires_at))} />}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
5
src/components/icons/ClockIcon.tsx
Normal file
5
src/components/icons/ClockIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Clock } from 'react-feather';
|
||||
|
||||
export default function ClockIcon({ ...props }) {
|
||||
return <Clock size={15} {...props} />;
|
||||
}
|
|
@ -22,6 +22,7 @@ import PlayIcon from './PlayIcon';
|
|||
import CalendarIcon from './CalendarIcon';
|
||||
import HashIcon from './HashIcon';
|
||||
import TagIcon from './TagIcon';
|
||||
import ClockIcon from './ClockIcon';
|
||||
|
||||
export {
|
||||
ActivityIcon,
|
||||
|
@ -48,4 +49,5 @@ export {
|
|||
CalendarIcon,
|
||||
HashIcon,
|
||||
TagIcon,
|
||||
ClockIcon,
|
||||
};
|
|
@ -1,13 +1,44 @@
|
|||
import { Button, Collapse, Group, Progress, Title } from '@mantine/core';
|
||||
import { Button, Collapse, Group, Progress, Select, Title } from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import Dropzone from 'components/dropzone/Dropzone';
|
||||
import FileDropzone from 'components/dropzone/DropzoneFile';
|
||||
import { CrossIcon, UploadIcon } from 'components/icons';
|
||||
import { ClockIcon, CrossIcon, UploadIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const expires = [
|
||||
'5min',
|
||||
'10min',
|
||||
'15min',
|
||||
'30min',
|
||||
'1h',
|
||||
'2h',
|
||||
'3h',
|
||||
'4h',
|
||||
'5h',
|
||||
'6h',
|
||||
'8h',
|
||||
'12h',
|
||||
'1d',
|
||||
'3d',
|
||||
'5d',
|
||||
'7d',
|
||||
'1w',
|
||||
'1.5w',
|
||||
'2w',
|
||||
'3w',
|
||||
'1m',
|
||||
'1.5m',
|
||||
'2m',
|
||||
'3m',
|
||||
'6m',
|
||||
'8m',
|
||||
'1y',
|
||||
'never',
|
||||
];
|
||||
|
||||
export default function Upload() {
|
||||
const clipboard = useClipboard();
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
@ -15,6 +46,7 @@ export default function Upload() {
|
|||
const [files, setFiles] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expires, setExpires] = useState('never');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('paste', (e: ClipboardEvent) => {
|
||||
|
@ -29,6 +61,36 @@ export default function Upload() {
|
|||
});
|
||||
|
||||
const handleUpload = async () => {
|
||||
const expires_at = expires === 'never' ? null : new Date({
|
||||
'5min': Date.now() + 5 * 60 * 1000,
|
||||
'10min': Date.now() + 10 * 60 * 1000,
|
||||
'15min': Date.now() + 15 * 60 * 1000,
|
||||
'30min': Date.now() + 30 * 60 * 1000,
|
||||
'1h': Date.now() + 60 * 60 * 1000,
|
||||
'2h': Date.now() + 2 * 60 * 60 * 1000,
|
||||
'3h': Date.now() + 3 * 60 * 60 * 1000,
|
||||
'4h': Date.now() + 4 * 60 * 60 * 1000,
|
||||
'5h': Date.now() + 5 * 60 * 60 * 1000,
|
||||
'6h': Date.now() + 6 * 60 * 60 * 1000,
|
||||
'8h': Date.now() + 8 * 60 * 60 * 1000,
|
||||
'12h': Date.now() + 12 * 60 * 60 * 1000,
|
||||
'1d': Date.now() + 24 * 60 * 60 * 1000,
|
||||
'3d': Date.now() + 3 * 24 * 60 * 60 * 1000,
|
||||
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
|
||||
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1w': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000,
|
||||
'2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000,
|
||||
'3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000,
|
||||
'1m': Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||
'1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000,
|
||||
'2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000,
|
||||
'3m': Date.now() + 3 * 30 * 24 * 60 * 60 * 1000,
|
||||
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
|
||||
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
|
||||
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
|
||||
}[expires]);
|
||||
|
||||
setProgress(0);
|
||||
setLoading(true);
|
||||
const body = new FormData();
|
||||
|
@ -78,6 +140,7 @@ export default function Upload() {
|
|||
|
||||
req.open('POST', '/api/upload');
|
||||
req.setRequestHeader('Authorization', user.token);
|
||||
req.setRequestHeader('Expires-At', expires_at.toISOString());
|
||||
req.send(body);
|
||||
};
|
||||
|
||||
|
@ -95,8 +158,43 @@ export default function Upload() {
|
|||
{progress !== 0 && <Progress mt='md' value={progress} animate />}
|
||||
</Collapse>
|
||||
|
||||
<Group position='right'>
|
||||
<Button leftIcon={<UploadIcon />} mt={12} onClick={handleUpload} disabled={files.length === 0 ? true : false}>Upload</Button>
|
||||
<Group position='right' mt='md'>
|
||||
<Select
|
||||
value={expires}
|
||||
onChange={(e) => setExpires(e)}
|
||||
icon={<ClockIcon size={14} />}
|
||||
data={[
|
||||
{ value: 'never', label: 'Never' },
|
||||
{ value: '5min', label: '5 minutes' },
|
||||
{ value: '10min', label: '10 minutes' },
|
||||
{ value: '15min', label: '15 minutes' },
|
||||
{ value: '30min', label: '30 minutes' },
|
||||
{ value: '1h', label: '1 hour' },
|
||||
{ value: '2h', label: '2 hours' },
|
||||
{ value: '3h', label: '3 hours' },
|
||||
{ value: '4h', label: '4 hours' },
|
||||
{ value: '5h', label: '5 hours' },
|
||||
{ value: '6h', label: '6 hours' },
|
||||
{ value: '8h', label: '8 hours' },
|
||||
{ value: '12h', label: '12 hours' },
|
||||
{ value: '1d', label: '1 day' },
|
||||
{ value: '3d', label: '3 days' },
|
||||
{ value: '5d', label: '5 days' },
|
||||
{ value: '7d', label: '7 days' },
|
||||
{ value: '1w', label: '1 week' },
|
||||
{ value: '1.5w', label: '1.5 weeks' },
|
||||
{ value: '2w', label: '2 weeks' },
|
||||
{ value: '3w', label: '3 weeks' },
|
||||
{ value: '1m', label: '1 month' },
|
||||
{ value: '1.5m', label: '1.5 months' },
|
||||
{ value: '2m', label: '2 months' },
|
||||
{ value: '3m', label: '3 months' },
|
||||
{ value: '6m', label: '6 months' },
|
||||
{ value: '8m', label: '8 months' },
|
||||
{ value: '1y', label: '1 year' },
|
||||
]}
|
||||
/>
|
||||
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>Upload</Button>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -27,4 +27,26 @@ export function bytesToRead(bytes: number) {
|
|||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const units = {
|
||||
year: 365 * 24 * 60 * 60 * 1000,
|
||||
month: 30 * 24 * 60 * 60 * 1000,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
|
||||
export function relativeTime(to: Date, from: Date = new Date()) {
|
||||
const time = new Date(to.getTime() - from.getTime());
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { style: 'long' });
|
||||
|
||||
for (const unit in units) {
|
||||
if (time > units[unit]) {
|
||||
return rtf.format(Math.floor(Math.round(time.getTime() / units[unit])), unit as Intl.RelativeTimeFormatUnit || 'second');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
if (req.files && req.files.length === 0) return res.error('no files');
|
||||
|
||||
const expires_at = req.headers['expires-at'] as string;
|
||||
const expiry = expires_at ? new Date(expires_at) : null;
|
||||
if (expiry) {
|
||||
if (!expiry.getTime()) return res.bad('invalid date');
|
||||
if (expiry.getTime() < Date.now()) return res.bad('date is in the past');
|
||||
}
|
||||
|
||||
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
|
||||
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
|
||||
const files = [];
|
||||
|
@ -84,6 +91,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
embed: !!req.headers.embed,
|
||||
format,
|
||||
password,
|
||||
expires_at: expiry,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
select: {
|
||||
created_at: true,
|
||||
expires_at: true,
|
||||
file: true,
|
||||
mimetype: true,
|
||||
id: true,
|
||||
|
|
|
@ -143,6 +143,14 @@ async function rawFileDb(
|
|||
prisma: PrismaClient,
|
||||
image: Image,
|
||||
) {
|
||||
if (image.expires_at && image.expires_at < new Date()) {
|
||||
Logger.get('server').info(`${image.file} expired`);
|
||||
await datasource.delete(image.file);
|
||||
await prisma.image.delete({ where: { id: image.id } });
|
||||
|
||||
return nextServer.render404(req, res as ServerResponse);
|
||||
}
|
||||
|
||||
const data = await datasource.get(image.file);
|
||||
if (!data) return nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
|
@ -169,6 +177,13 @@ async function fileDb(
|
|||
handle: RequestHandler,
|
||||
image: Image,
|
||||
) {
|
||||
if (image.expires_at && image.expires_at < new Date()) {
|
||||
await datasource.delete(image.file);
|
||||
await prisma.image.delete({ where: { id: image.id } });
|
||||
|
||||
return nextServer.render404(req, res as ServerResponse);
|
||||
}
|
||||
|
||||
const ext = image.file.split('.').pop();
|
||||
if (Object.keys(exts).includes(ext)) return handle(req, res as ServerResponse);
|
||||
|
||||
|
|
Loading…
Reference in a new issue