feat: more ways to expire

This commit is contained in:
diced 2022-08-21 22:24:56 -07:00
parent e911db4c1a
commit 4f631fbd0e
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
8 changed files with 86 additions and 17 deletions

View file

@ -4,7 +4,7 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 3.4.4 | :white_check_mark: | | 3.4.8 | :white_check_mark: |
| < 3 | :x: | | < 3 | :x: |
| < 2 | :x: | | < 2 | :x: |

View file

@ -42,6 +42,7 @@
"fflate": "^0.7.3", "fflate": "^0.7.3",
"find-my-way": "^6.3.0", "find-my-way": "^6.3.0",
"minio": "^7.0.28", "minio": "^7.0.28",
"ms": "canary",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"next": "^12.1.6", "next": "^12.1.6",
"prisma": "^4.1.0", "prisma": "^4.1.0",

View file

@ -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 { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
@ -8,8 +8,18 @@ import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, Has
import MutedText from './MutedText'; import MutedText from './MutedText';
import { relativeTime } from 'lib/clientUtils'; import { relativeTime } from 'lib/clientUtils';
export function FileMeta({ Icon, title, subtitle }) { export function FileMeta({ Icon, title, subtitle, ...other }) {
return ( 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> <Group>
<Icon size={24} /> <Icon size={24} />
<Stack spacing={1}> <Stack spacing={1}>
@ -88,7 +98,12 @@ export default function File({ image, updateImages }) {
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} /> <FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} /> <FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} /> <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} /> <FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</Stack> </Stack>
</Stack> </Stack>

View file

@ -140,7 +140,7 @@ export default function Upload() {
req.open('POST', '/api/upload'); req.open('POST', '/api/upload');
req.setRequestHeader('Authorization', user.token); 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); req.send(body);
}; };

View file

@ -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) { export function parse(str: string, image: Image, user: User) {
if (!str) return null; 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;
}

View file

@ -9,6 +9,8 @@ import { format as formatDate } from 'fecha';
import datasource from 'lib/datasource'; import datasource from 'lib/datasource';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import sharp from 'sharp'; import sharp from 'sharp';
import { humanTime, parseExpiry } from 'lib/clientUtils';
import { StringValue } from 'ms';
const uploader = multer(); const uploader = multer();
@ -42,21 +44,27 @@ async function handler(req: NextApiReq, res: NextApiRes) {
await run(uploader.array('file'))(req, res); await run(uploader.array('file'))(req, res);
if (!req.files) return res.error('no files'); if (!req.files) return res.error('no files');
if (req.files && req.files.length === 0) 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 expires_at = req.headers['expires-at'] as string;
const expiry = expires_at ? new Date(expires_at) : null; let expiry: Date;
if (expiry) {
if (!expiry.getTime()) return res.bad('invalid date'); if (expires_at) {
if (expiry.getTime() < Date.now()) return res.bad('date is in the past'); 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 rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat]; 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 imageCompressionPercent = req.headers['image-compression-percent'] ? Number(req.headers['image-compression-percent']) : null;
const files = [];
for (let i = 0; i !== req.files.length; ++i) { for (let i = 0; i !== req.files.length; ++i) {
const file = req.files[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`); 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: case ImageFormat.NAME:
fileName = file.originalname.split('.')[0]; fileName = file.originalname.split('.')[0];
break; break;
default:
fileName = randomChars(zconfig.uploader.length);
break;
} }
let password = null; let password = null;
@ -90,7 +101,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const image = await prisma.image.create({ const image = await prisma.image.create({
data: { data: {
file: `${fileName}.${compressionUsed ? 'jpg' : ext}`, 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, userId: user.id,
embed: !!req.headers.embed, embed: !!req.headers.embed,
format, 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})`); Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
if (user.domains.length) { if (user.domains.length) {
const domain = user.domains[Math.floor(Math.random() * 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 { } 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) { function run(middleware: any) {

View file

@ -19,6 +19,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}, },
select: { select: {
created_at: true, created_at: true,
expires_at: true,
file: true, file: true,
mimetype: true, mimetype: true,
id: true, id: true,

View file

@ -6009,6 +6009,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "mssql@npm:8.1.2":
version: 8.1.2 version: 8.1.2
resolution: "mssql@npm:8.1.2" resolution: "mssql@npm:8.1.2"
@ -8896,6 +8903,7 @@ __metadata:
fflate: ^0.7.3 fflate: ^0.7.3
find-my-way: ^6.3.0 find-my-way: ^6.3.0
minio: ^7.0.28 minio: ^7.0.28
ms: canary
multer: ^1.4.5-lts.1 multer: ^1.4.5-lts.1
next: ^12.1.6 next: ^12.1.6
npm-run-all: ^4.1.5 npm-run-all: ^4.1.5