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 |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 3.4.4 | :white_check_mark: |
|
| 3.4.8 | :white_check_mark: |
|
||||||
| < 3 | :x: |
|
| < 3 | :x: |
|
||||||
| < 2 | :x: |
|
| < 2 | :x: |
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue