feat: new file serving method & max views for files
This commit is contained in:
parent
d5984f4141
commit
7eb855de8f
11 changed files with 197 additions and 161 deletions
2
prisma/migrations/20221028005627_max_views/migration.sql
Normal file
2
prisma/migrations/20221028005627_max_views/migration.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "maxViews" INTEGER;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "maxViews" INTEGER;
|
|
@ -42,6 +42,7 @@ model Image {
|
|||
mimetype String @default("image/png")
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime?
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
|
@ -64,6 +65,7 @@ model Url {
|
|||
destination String
|
||||
vanity String?
|
||||
created_at DateTime @default(now())
|
||||
maxViews Int?
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
|
|
@ -128,7 +128,15 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
<Stack>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views.toLocaleString()} />
|
||||
{image.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={image.maxViews.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${image.maxViews.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded at'
|
||||
|
@ -147,12 +155,12 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
</Stack>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Link href={image.url} target='_blank'>
|
||||
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
|
||||
</Link>
|
||||
<Button onClick={handleCopy}>Copy URL</Button>
|
||||
<Button onClick={handleDelete}>Delete</Button>
|
||||
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
<Link href={image.url} target='_blank'>
|
||||
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
|
||||
</Link>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
import { Button, Collapse, Group, Progress, Select, Title, PasswordInput, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Group,
|
||||
Progress,
|
||||
Select,
|
||||
Title,
|
||||
PasswordInput,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import Dropzone from 'components/dropzone/Dropzone';
|
||||
|
@ -20,6 +30,8 @@ export default function Upload({ chunks: chunks_config }) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [expires, setExpires] = useState('never');
|
||||
const [password, setPassword] = useState('');
|
||||
const [maxViews, setMaxViews] = useState<number>(undefined);
|
||||
console.log(maxViews);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('paste', (e: ClipboardEvent) => {
|
||||
|
@ -139,6 +151,7 @@ export default function Upload({ chunks: chunks_config }) {
|
|||
req.setRequestHeader('X-Zipline-Partial-LastChunk', j === chunks.length - 1 ? 'true' : 'false');
|
||||
expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString());
|
||||
password !== '' && req.setRequestHeader('Password', password);
|
||||
maxViews && maxViews !== 0 && req.setRequestHeader('Max-Views', String(maxViews));
|
||||
|
||||
req.send(body);
|
||||
|
||||
|
@ -285,6 +298,7 @@ export default function Upload({ chunks: chunks_config }) {
|
|||
req.setRequestHeader('Authorization', user.token);
|
||||
expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString());
|
||||
password !== '' && req.setRequestHeader('Password', password);
|
||||
maxViews && maxViews !== 0 && req.setRequestHeader('Max-Views', String(maxViews));
|
||||
|
||||
req.send(body);
|
||||
}
|
||||
|
@ -307,6 +321,9 @@ export default function Upload({ chunks: chunks_config }) {
|
|||
</Collapse>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Tooltip label='After the file reaches this amount of views, it will be deleted automatically. Leave blank for no limit.'>
|
||||
<NumberInput placeholder='Max Views' min={0} value={maxViews} onChange={(x) => setMaxViews(x)} />
|
||||
</Tooltip>
|
||||
<Tooltip label='Add a password to your files (optional, leave blank for none)'>
|
||||
<PasswordInput
|
||||
style={{ width: '252px' }}
|
||||
|
|
|
@ -19,6 +19,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
if (!user) return res.forbid('authorization incorect');
|
||||
if (!req.body) return res.error('no body');
|
||||
if (!req.body.url) return res.error('no url');
|
||||
|
||||
const maxUrlViews = req.headers['max-views'] ? Number(req.headers['max-views']) : null;
|
||||
if (isNaN(maxUrlViews)) return res.error('invalid max views (invalid number)');
|
||||
if (maxUrlViews < 0) return res.error('invalid max views (max views < 0)');
|
||||
|
||||
const rand = randomChars(zconfig.urls.length);
|
||||
|
||||
let invis;
|
||||
|
@ -39,6 +44,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
vanity: req.body.vanity ?? null,
|
||||
destination: req.body.url,
|
||||
userId: user.id,
|
||||
maxViews: maxUrlViews,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import multer from 'multer';
|
||||
import prisma from 'lib/prisma';
|
||||
import zconfig from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvisImage, randomChars, hashPassword } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import { ImageFormat, InvisibleImage } from '@prisma/client';
|
||||
import dayjs from 'dayjs';
|
||||
import datasource from 'lib/datasource';
|
||||
import { randomUUID } from 'crypto';
|
||||
import sharp from 'sharp';
|
||||
import { parseExpiry } from 'lib/utils/client';
|
||||
import { sendUpload } from 'lib/discord';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import dayjs from 'dayjs';
|
||||
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
|
||||
import { guess } from 'lib/mimes';
|
||||
import zconfig from 'lib/config';
|
||||
import datasource from 'lib/datasource';
|
||||
import { sendUpload } from 'lib/discord';
|
||||
import Logger from 'lib/logger';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import { createInvisImage, hashPassword, randomChars } from 'lib/util';
|
||||
import { parseExpiry } from 'lib/utils/client';
|
||||
import multer from 'multer';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const uploader = multer();
|
||||
|
||||
|
@ -50,6 +49,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
const imageCompressionPercent = req.headers['image-compression-percent']
|
||||
? Number(req.headers['image-compression-percent'])
|
||||
: null;
|
||||
if (isNaN(imageCompressionPercent)) return res.error('invalid image compression percent (invalid number)');
|
||||
if (imageCompressionPercent < 0 || imageCompressionPercent > 100)
|
||||
return res.error('invalid image compression percent (% < 0 || % > 100)');
|
||||
|
||||
const fileMaxViews = req.headers['max-views'] ? Number(req.headers['max-views']) : null;
|
||||
if (isNaN(fileMaxViews)) return res.error('invalid max views (invalid number)');
|
||||
if (fileMaxViews < 0) return res.error('invalid max views (max views < 0)');
|
||||
|
||||
// handle partial uploads before ratelimits
|
||||
if (req.headers['content-range']) {
|
||||
|
@ -130,6 +136,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
format,
|
||||
password,
|
||||
expires_at: expiry,
|
||||
maxViews: fileMaxViews,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -141,7 +148,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
files: [
|
||||
`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${
|
||||
zconfig.uploader.route === '/' ? '' : zconfig.uploader.route
|
||||
}/${file.file}`,
|
||||
}/${invis ? invis.invis : file.file}`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -173,11 +180,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
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`);
|
||||
return res.error(`file[${i}]: size too big`);
|
||||
|
||||
const ext = file.originalname.split('.').pop();
|
||||
if (zconfig.uploader.disabled_extensions.includes(ext))
|
||||
return res.error('disabled extension recieved: ' + ext);
|
||||
return res.error(`file[${i}]: disabled extension recieved: ${ext}`);
|
||||
let fileName: string;
|
||||
|
||||
switch (format) {
|
||||
|
@ -214,6 +221,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
format,
|
||||
password,
|
||||
expires_at: expiry,
|
||||
maxViews: fileMaxViews,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
id: true,
|
||||
favorite: true,
|
||||
views: true,
|
||||
maxViews: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import config from 'lib/config';
|
|||
import prisma from 'lib/prisma';
|
||||
import { parse } from 'lib/utils/client';
|
||||
import exts from 'lib/exts';
|
||||
import { Image } from '@prisma/client';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function EmbeddedImage({ image, user, pass, prismRender }) {
|
||||
|
@ -18,7 +17,7 @@ export default function EmbeddedImage({ image, user, pass, prismRender }) {
|
|||
const [error, setError] = useState('');
|
||||
|
||||
// reapply date from workaround
|
||||
image.created_at = new Date(image.created_at);
|
||||
image.created_at = new Date(image?.created_at);
|
||||
|
||||
const check = async () => {
|
||||
const res = await fetch(`/api/auth/image?id=${image.id}&password=${password}`);
|
||||
|
@ -139,93 +138,50 @@ export default function EmbeddedImage({ image, user, pass, prismRender }) {
|
|||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const route = context.params.id[0];
|
||||
const serve_on_root = /(^[^\\.]+\.[^\\.]+)/.test(route);
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
const id = serve_on_root ? route : context.params.id[1];
|
||||
const uploader_route = config.uploader.route.substring(1);
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [{ file: id }, { invisible: { invis: id } }],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
userId: true,
|
||||
embed: true,
|
||||
created_at: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
if (!image) return { notFound: true };
|
||||
|
||||
if (route === config.urls.route.substring(1)) {
|
||||
const url = await prisma.url.findFirst({
|
||||
where: {
|
||||
OR: [{ id }, { vanity: id }, { invisible: { invis: id } }],
|
||||
},
|
||||
select: {
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
if (!url) return { notFound: true };
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
embedTitle: true,
|
||||
embedColor: true,
|
||||
embedSiteName: true,
|
||||
username: true,
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: image.userId,
|
||||
},
|
||||
});
|
||||
|
||||
//@ts-ignore workaround because next wont allow date
|
||||
image.created_at = image.created_at.toString();
|
||||
|
||||
const prismRender = Object.keys(exts).includes(image.file.split('.').pop());
|
||||
if (prismRender && !image.password)
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: url.destination,
|
||||
destination: `/code/${image.file}`,
|
||||
permanent: true,
|
||||
},
|
||||
};
|
||||
} else if (uploader_route === '' ? /(^[^\\.]+\.[^\\.]+)/.test(route) : route === uploader_route) {
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [{ file: id }, { invisible: { invis: id } }],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
userId: true,
|
||||
embed: true,
|
||||
created_at: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
if (!image) return { notFound: true };
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
embedTitle: true,
|
||||
embedColor: true,
|
||||
embedSiteName: true,
|
||||
username: true,
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: image.userId,
|
||||
},
|
||||
});
|
||||
|
||||
//@ts-ignore workaround because next wont allow date
|
||||
image.created_at = image.created_at.toString();
|
||||
|
||||
const prismRender = Object.keys(exts).includes(image.file.split('.').pop());
|
||||
if (prismRender && !image.password)
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/code/${image.file}`,
|
||||
permanent: true,
|
||||
},
|
||||
};
|
||||
else if (prismRender && image.password) {
|
||||
const pass = image.password ? true : false;
|
||||
delete image.password;
|
||||
return {
|
||||
props: {
|
||||
image,
|
||||
user,
|
||||
pass,
|
||||
prismRender: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!image.mimetype.startsWith('image') && !image.mimetype.startsWith('video')) {
|
||||
const { default: datasource } = await import('lib/datasource');
|
||||
|
||||
const data = await datasource.get(image.file);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
data.pipe(context.res);
|
||||
return { props: {} };
|
||||
}
|
||||
else if (prismRender && image.password) {
|
||||
const pass = image.password ? true : false;
|
||||
delete image.password;
|
||||
return {
|
||||
|
@ -233,9 +189,27 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
image,
|
||||
user,
|
||||
pass,
|
||||
prismRender: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
if (!image.mimetype.startsWith('image') && !image.mimetype.startsWith('video')) {
|
||||
const { default: datasource } = await import('lib/datasource');
|
||||
|
||||
const data = await datasource.get(image.file);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
data.pipe(context.res);
|
||||
return { props: {} };
|
||||
}
|
||||
const pass = image.password ? true : false;
|
||||
delete image.password;
|
||||
return {
|
||||
props: {
|
||||
image,
|
||||
user,
|
||||
pass,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -5,13 +5,14 @@ import { Image, PrismaClient } from '@prisma/client';
|
|||
import { createServer, IncomingMessage, OutgoingMessage, ServerResponse } from 'http';
|
||||
import { extname } from 'path';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { getStats, log, migrations } from './util';
|
||||
import { getStats, log, migrations, redirect } from './util';
|
||||
import Logger from '../lib/logger';
|
||||
import { guess } from '../lib/mimes';
|
||||
import exts from '../lib/exts';
|
||||
import { version } from '../../package.json';
|
||||
import config from '../lib/config';
|
||||
import datasource from '../lib/datasource';
|
||||
import { NextUrlWithParsedQuery } from 'next/dist/server/request-meta';
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
const logger = Logger.get('server');
|
||||
|
@ -78,26 +79,54 @@ async function start() {
|
|||
},
|
||||
});
|
||||
|
||||
router.on(
|
||||
'GET',
|
||||
config.uploader.route === '/' ? '/:id(^[^\\.]+\\.[^\\.]+)' : `${config.uploader.route}/:id`,
|
||||
async (req, res, params) => {
|
||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||
router.on('GET', `${config.urls.route}/:id`, async (req, res, params) => {
|
||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
const image = await prisma.image.findFirst({
|
||||
const url = await prisma.url.findFirst({
|
||||
where: {
|
||||
OR: [{ id: params.id }, { vanity: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||
},
|
||||
});
|
||||
if (!url) return nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
const nUrl = await prisma.url.update({
|
||||
where: {
|
||||
id: url.id,
|
||||
},
|
||||
data: {
|
||||
views: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
if (nUrl.maxViews && nUrl.views >= nUrl.maxViews) {
|
||||
await prisma.url.delete({
|
||||
where: {
|
||||
OR: [{ file: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||
id: nUrl.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) await rawFile(req, res, nextServer, params.id);
|
||||
else {
|
||||
if (image.password) await handle(req, res);
|
||||
else if (image.embed) await handle(req, res);
|
||||
else await fileDb(req, res, nextServer, prisma, handle, image);
|
||||
}
|
||||
return nextServer.render404(req, res as ServerResponse);
|
||||
}
|
||||
);
|
||||
|
||||
return redirect(res, url.destination);
|
||||
});
|
||||
|
||||
router.on('GET', `${config.uploader.route}/:id`, async (req, res, params) => {
|
||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [{ file: params.id }, { invisible: { invis: decodeURI(params.id) } }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) await rawFile(req, res, nextServer, params.id);
|
||||
else {
|
||||
if (image.password) return redirect(res, `/view/${image.file}`);
|
||||
else if (image.embed) await handle(req, res);
|
||||
else await fileDb(req, res, nextServer, prisma, handle, image);
|
||||
}
|
||||
});
|
||||
|
||||
router.on('GET', '/r/:id', async (req, res, params) => {
|
||||
if (params.id === '') return nextServer.render404(req, res as ServerResponse);
|
||||
|
@ -110,8 +139,13 @@ async function start() {
|
|||
|
||||
if (!image) await rawFile(req, res, nextServer, params.id);
|
||||
else {
|
||||
if (image.password) await handle(req, res);
|
||||
else await rawFileDb(req, res, nextServer, prisma, image);
|
||||
if (image.password) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.statusCode = 403;
|
||||
return res.end(
|
||||
JSON.stringify({ error: "can't view a raw file that has a password", url: `/view/${image.file}` })
|
||||
);
|
||||
} else await rawFile(req, res, nextServer, params.id);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -167,39 +201,6 @@ async function rawFile(req: IncomingMessage, res: OutgoingMessage, nextServer: N
|
|||
data.on('end', () => res.end());
|
||||
}
|
||||
|
||||
async function rawFileDb(
|
||||
req: IncomingMessage,
|
||||
res: OutgoingMessage,
|
||||
nextServer: NextServer,
|
||||
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);
|
||||
|
||||
const size = await datasource.size(image.file);
|
||||
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.setHeader('Content-Length', size);
|
||||
|
||||
data.pipe(res);
|
||||
data.on('error', () => nextServer.render404(req, res as ServerResponse));
|
||||
data.on('end', () => res.end());
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
async function fileDb(
|
||||
req: IncomingMessage,
|
||||
res: OutgoingMessage,
|
||||
|
@ -221,6 +222,20 @@ async function fileDb(
|
|||
const data = await datasource.get(image.file);
|
||||
if (!data) return nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
const nImage = await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
|
||||
if (nImage.maxViews && nImage.views >= nImage.maxViews) {
|
||||
await datasource.delete(image.file);
|
||||
await prisma.image.delete({ where: { id: image.id } });
|
||||
|
||||
Logger.get('image').info(`Image ${image.file} has been deleted due to max views (${nImage.maxViews})`);
|
||||
|
||||
return nextServer.render404(req, res as ServerResponse);
|
||||
}
|
||||
|
||||
const size = await datasource.size(image.file);
|
||||
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
|
@ -228,11 +243,6 @@ async function fileDb(
|
|||
data.pipe(res);
|
||||
data.on('error', () => nextServer.render404(req, res as ServerResponse));
|
||||
data.on('end', () => res.end());
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
async function stats(prisma: PrismaClient) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Logger from '../lib/logger';
|
|||
import { bytesToHuman } from '../lib/utils/bytes';
|
||||
import { Datasource } from '../lib/datasources';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { ServerResponse } from 'http';
|
||||
|
||||
export async function migrations() {
|
||||
try {
|
||||
|
@ -37,6 +38,11 @@ export function log(url: string) {
|
|||
return Logger.get('url').info(url);
|
||||
}
|
||||
|
||||
export function redirect(res: ServerResponse, url: string) {
|
||||
res.writeHead(307, { Location: url });
|
||||
res.end();
|
||||
}
|
||||
|
||||
export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||
const size = await datasource.fullSize();
|
||||
const byUser = await prisma.image.groupBy({
|
||||
|
|
Loading…
Reference in a new issue