feat: more ways to expire
This commit is contained in:
parent
e911db4c1a
commit
4f631fbd0e
8 changed files with 86 additions and 17 deletions
|
@ -4,7 +4,7 @@
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.4.4 | :white_check_mark: |
|
||||
| 3.4.8 | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 2 | :x: |
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"fflate": "^0.7.3",
|
||||
"find-my-way": "^6.3.0",
|
||||
"minio": "^7.0.28",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^12.1.6",
|
||||
"prisma": "^4.1.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Card, Group, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
||||
import { Button, Card, Group, Modal, Stack, Text, Title, Tooltip, useMantineTheme } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
@ -8,8 +8,18 @@ import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, Has
|
|||
import MutedText from './MutedText';
|
||||
import { relativeTime } from 'lib/clientUtils';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle }) {
|
||||
return (
|
||||
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}>
|
||||
|
@ -88,7 +98,12 @@ 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))} />}
|
||||
{image.expires_at && <FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(image.expires_at))}
|
||||
tooltip={new Date(image.expires_at).toLocaleString()}
|
||||
/>}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
|
@ -140,7 +140,7 @@ export default function Upload() {
|
|||
|
||||
req.open('POST', '/api/upload');
|
||||
req.setRequestHeader('Authorization', user.token);
|
||||
expires !== 'never' && req.setRequestHeader('Expires-At', expires_at.toISOString());
|
||||
expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString());
|
||||
|
||||
req.send(body);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Image, User } from '@prisma/client';
|
||||
import type { Image, ImageFormat, User } from '@prisma/client';
|
||||
import ms, { StringValue } from 'ms';
|
||||
|
||||
export function parse(str: string, image: Image, user: User) {
|
||||
if (!str) return null;
|
||||
|
@ -50,3 +51,35 @@ export function relativeTime(to: Date, from: Date = new Date()) {
|
|||
}
|
||||
}
|
||||
|
||||
export function humanTime(string: StringValue | string): Date {
|
||||
try {
|
||||
const mil = ms(string as StringValue);
|
||||
if (typeof mil !== 'number') return null;
|
||||
if (isNaN(mil)) return null;
|
||||
if (!mil) return null;
|
||||
|
||||
return new Date(Date.now() + mil);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseExpiry(header: string): Date | null {
|
||||
if (!header) return null;
|
||||
header = header.toLowerCase();
|
||||
|
||||
if (header.startsWith('date=')) {
|
||||
const date = new Date(header.substring(5));
|
||||
|
||||
if (!date.getTime()) return null;
|
||||
if (date.getTime() < Date.now()) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
const human = humanTime(header);
|
||||
|
||||
if (!human) return null;
|
||||
if (human.getTime() < Date.now()) return null;
|
||||
|
||||
return human;
|
||||
}
|
|
@ -9,6 +9,8 @@ import { format as formatDate } from 'fecha';
|
|||
import datasource from 'lib/datasource';
|
||||
import { randomUUID } from 'crypto';
|
||||
import sharp from 'sharp';
|
||||
import { humanTime, parseExpiry } from 'lib/clientUtils';
|
||||
import { StringValue } from 'ms';
|
||||
|
||||
const uploader = multer();
|
||||
|
||||
|
@ -42,21 +44,27 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
await run(uploader.array('file'))(req, res);
|
||||
|
||||
if (!req.files) return res.error('no files');
|
||||
|
||||
if (req.files && req.files.length === 0) return res.error('no files');
|
||||
|
||||
const response: { files: string[], expires_at?: Date } = { 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');
|
||||
let expiry: Date;
|
||||
|
||||
if (expires_at) {
|
||||
expiry = parseExpiry(expires_at);
|
||||
if (!expiry) return res.error('invalid date');
|
||||
else {
|
||||
response.expires_at = expiry;
|
||||
}
|
||||
}
|
||||
|
||||
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
|
||||
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
|
||||
|
||||
const imageCompressionPercent = req.headers['image-compression-percent'] ? Number(req.headers['image-compression-percent']) : null;
|
||||
|
||||
const files = [];
|
||||
|
||||
for (let i = 0; i !== req.files.length; ++i) {
|
||||
const file = req.files[i];
|
||||
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error(`file[${i}] size too big`);
|
||||
|
@ -78,6 +86,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
case ImageFormat.NAME:
|
||||
fileName = file.originalname.split('.')[0];
|
||||
break;
|
||||
default:
|
||||
fileName = randomChars(zconfig.uploader.length);
|
||||
break;
|
||||
}
|
||||
|
||||
let password = null;
|
||||
|
@ -90,7 +101,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
const image = await prisma.image.create({
|
||||
data: {
|
||||
file: `${fileName}.${compressionUsed ? 'jpg' : ext}`,
|
||||
mimetype: req.headers.uploadtext ? 'text/plain' : file.mimetype,
|
||||
mimetype: req.headers.uploadtext ? 'text/plain' : (compressionUsed ? 'image/jpeg' : file.mimetype),
|
||||
userId: user.id,
|
||||
embed: !!req.headers.embed,
|
||||
format,
|
||||
|
@ -112,9 +123,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
if (user.domains.length) {
|
||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||
files.push(`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
response.files.push(`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
} else {
|
||||
files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
response.files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +151,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
}
|
||||
}
|
||||
|
||||
return res.json({ files });
|
||||
return res.json(response);
|
||||
}
|
||||
|
||||
function run(middleware: any) {
|
||||
|
|
|
@ -19,6 +19,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
},
|
||||
select: {
|
||||
created_at: true,
|
||||
expires_at: true,
|
||||
file: true,
|
||||
mimetype: true,
|
||||
id: true,
|
||||
|
|
|
@ -6009,6 +6009,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:canary":
|
||||
version: 3.0.0-canary.1
|
||||
resolution: "ms@npm:3.0.0-canary.1"
|
||||
checksum: 5ec76c0932cf83ac3e7f70f1a4c0d4db4dbc91de6ea5f7d336c67b48f513c8cb4c0fce3a07e3d84ee931dbdc9a48f33ed1c485e834279fff8906d385e86684ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mssql@npm:8.1.2":
|
||||
version: 8.1.2
|
||||
resolution: "mssql@npm:8.1.2"
|
||||
|
@ -8896,6 +8903,7 @@ __metadata:
|
|||
fflate: ^0.7.3
|
||||
find-my-way: ^6.3.0
|
||||
minio: ^7.0.28
|
||||
ms: canary
|
||||
multer: ^1.4.5-lts.1
|
||||
next: ^12.1.6
|
||||
npm-run-all: ^4.1.5
|
||||
|
|
Loading…
Reference in a new issue