diff --git a/SECURITY.md b/SECURITY.md
index 358a5c2..9f5e525 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,7 +4,7 @@
| Version | Supported |
| ------- | ------------------ |
-| 3.4.4 | :white_check_mark: |
+| 3.4.8 | :white_check_mark: |
| < 3 | :x: |
| < 2 | :x: |
diff --git a/package.json b/package.json
index 06bc351..7c81942 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/File.tsx b/src/components/File.tsx
index f49dcda..f0e36ed 100644
--- a/src/components/File.tsx
+++ b/src/components/File.tsx
@@ -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 ? (
+
+
+
+
+ {title}
+ {subtitle}
+
+
+
+ ) : (
@@ -88,7 +98,12 @@ export default function File({ image, updateImages }) {
- {image.expires_at && }
+ {image.expires_at && }
diff --git a/src/components/pages/Upload.tsx b/src/components/pages/Upload.tsx
index 49198c2..6417a3a 100644
--- a/src/components/pages/Upload.tsx
+++ b/src/components/pages/Upload.tsx
@@ -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);
};
diff --git a/src/lib/clientUtils.ts b/src/lib/clientUtils.ts
index e1b09dc..17851a2 100644
--- a/src/lib/clientUtils.ts
+++ b/src/lib/clientUtils.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts
index f7ea556..ad68298 100644
--- a/src/pages/api/upload.ts
+++ b/src/pages/api/upload.ts
@@ -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) {
diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts
index cc42f00..fd0f7d8 100644
--- a/src/pages/api/user/recent.ts
+++ b/src/pages/api/user/recent.ts
@@ -19,6 +19,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
},
select: {
created_at: true,
+ expires_at: true,
file: true,
mimetype: true,
id: true,
diff --git a/yarn.lock b/yarn.lock
index 4062406..25ad837 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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