mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 15:50:11 -08:00
fix: new view counting method
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import { parseRange } from '@/lib/api/range';
|
||||
import { config } from '@/lib/config';
|
||||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { rawFileHandler } from './raw/[id]';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
@@ -15,14 +11,11 @@ type Query = {
|
||||
download?: string;
|
||||
};
|
||||
|
||||
const logger = log('routes').c('files');
|
||||
|
||||
export async function filesRoute(
|
||||
req: FastifyRequest<{ Params: Params; Querystring: Query }>,
|
||||
res: FastifyReply,
|
||||
) {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: decodeURIComponent(id),
|
||||
@@ -33,120 +26,8 @@ export async function filesRoute(
|
||||
});
|
||||
if (!file) return res.callNotFound();
|
||||
|
||||
if (file.deletesAt && file.deletesAt <= new Date()) {
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger
|
||||
.error('failed to delete file on expiration', {
|
||||
id: file.id,
|
||||
})
|
||||
.error(e as Error);
|
||||
}
|
||||
return res.callNotFound();
|
||||
}
|
||||
if (file.maxViews && file.views >= file.maxViews) {
|
||||
if (!config.features.deleteOnMaxViews) return res.callNotFound();
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger
|
||||
.error('failed to delete file on max views', {
|
||||
id: file.id,
|
||||
})
|
||||
.error(e as Error);
|
||||
}
|
||||
return res.callNotFound();
|
||||
}
|
||||
if (file.User?.view.enabled) return res.redirect(`/view/${encodeURIComponent(file.name)}`);
|
||||
if (file.type.startsWith('text/')) return res.redirect(`/view/${encodeURIComponent(file.name)}`);
|
||||
const stream = await datasource.get(file.name);
|
||||
if (!stream) return res.callNotFound();
|
||||
if (file.password) {
|
||||
if (!pw) return res.redirect(`/view/${encodeURIComponent(file.name)}`);
|
||||
const verified = await verifyPassword(pw as string, file.password!);
|
||||
if (!verified) {
|
||||
logger.warn('password protected file accessed with an incorrect password', { id: file.id, ip: req.ip });
|
||||
return res.callNotFound();
|
||||
}
|
||||
}
|
||||
if (!req.headers.range) {
|
||||
await prisma.file.update({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
if (!buf) return res.callNotFound();
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
|
||||
return rawFileHandler(req, res);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ import { verifyPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
const viewsCache = new Map<string, number>();
|
||||
const VIEW_WINDOW = 5 * 1000;
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
@@ -17,154 +21,152 @@ type Querystring = {
|
||||
|
||||
const logger = log('routes').c('raw');
|
||||
|
||||
export const rawFileHandler = async (
|
||||
req: FastifyRequest<{
|
||||
Params: Params;
|
||||
Querystring: Querystring;
|
||||
}>,
|
||||
res: FastifyReply,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: decodeURIComponent(id),
|
||||
},
|
||||
});
|
||||
|
||||
if (!file) return res.callNotFound();
|
||||
|
||||
if (file?.deletesAt && file.deletesAt <= new Date()) {
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('failed to delete file on expiration', { id: file.id }).error(e as Error);
|
||||
}
|
||||
return res.callNotFound();
|
||||
}
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) return res.forbidden('Password protected.');
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
|
||||
if (!verified) return res.forbidden('Incorrect password.');
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
// view stuff
|
||||
const now = Date.now();
|
||||
const isView = !req.headers.range || req.headers.range.startsWith('bytes=0');
|
||||
const key = `${req.ip}-${req.headers['user-agent'] ?? 'unknown'}-${file.id}`;
|
||||
const last = viewsCache.get(key) || 0;
|
||||
|
||||
const canCountView = isView && now - last > VIEW_WINDOW;
|
||||
const updatedViews = (file.views || 0) + (canCountView ? 1 : 0);
|
||||
|
||||
// check using future values
|
||||
if (file.maxViews && updatedViews > file.maxViews) {
|
||||
if (config.features.deleteOnMaxViews) {
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: { id: file.id },
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('failed to delete file on max views', { id: file.id }).error(e as Error);
|
||||
}
|
||||
}
|
||||
return res.callNotFound();
|
||||
}
|
||||
|
||||
const countView = async () => {
|
||||
if (!file || !canCountView) return;
|
||||
viewsCache.set(key, now);
|
||||
|
||||
try {
|
||||
await prisma.file.update({
|
||||
where: { id: file.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('failed to increment view counter', { id: file.id }).error(e as Error);
|
||||
}
|
||||
};
|
||||
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
await countView();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && { 'Content-Disposition': 'attachment;' }),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
await countView();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && { 'Content-Disposition': 'attachment;' }),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
await countView();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && { 'Content-Disposition': 'attachment;' }),
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
};
|
||||
|
||||
export const PATH = '/raw/:id';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{
|
||||
Querystring: Querystring;
|
||||
Params: Params;
|
||||
}>(
|
||||
PATH,
|
||||
{
|
||||
onResponse: async (req) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
await prisma.file.updateMany({
|
||||
where: {
|
||||
name: decodeURIComponent(id),
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { pw, download } = req.query;
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
name: decodeURIComponent(id),
|
||||
},
|
||||
});
|
||||
|
||||
if (file?.deletesAt && file.deletesAt <= new Date()) {
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger
|
||||
.error('failed to delete file on expiration', {
|
||||
id: file.id,
|
||||
})
|
||||
.error(e as Error);
|
||||
}
|
||||
|
||||
return res.callNotFound();
|
||||
}
|
||||
|
||||
if (file?.maxViews && file.views >= file.maxViews) {
|
||||
if (!config.features.deleteOnMaxViews) return res.callNotFound();
|
||||
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
where: {
|
||||
id: file.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger
|
||||
.error('failed to delete file on max views', {
|
||||
id: file.id,
|
||||
})
|
||||
.error(e as Error);
|
||||
}
|
||||
|
||||
return res.callNotFound();
|
||||
}
|
||||
|
||||
if (file?.password) {
|
||||
if (!pw) return res.forbidden('Password protected.');
|
||||
const verified = await verifyPassword(pw, file.password!);
|
||||
|
||||
if (!verified) return res.forbidden('Incorrect password.');
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
if (!buf) return res.callNotFound();
|
||||
|
||||
return res
|
||||
.type(file?.type || 'application/octet-stream')
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(file.originalName)}"`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
},
|
||||
);
|
||||
server.get(PATH, rawFileHandler);
|
||||
|
||||
done();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user