81 lines
2.4 KiB
JavaScript
81 lines
2.4 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { Readable } from 'node:stream';
|
|
|
|
import { NextResponse } from 'next/server';
|
|
|
|
import { shareDir } from '@/src/lib/config.js';
|
|
import { get, logEvent, run, runCleanupIfNeeded } from '@/src/lib/db.js';
|
|
import { getRequestMeta } from '@/src/lib/security.js';
|
|
|
|
export const runtime = 'nodejs';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function safeFilename(value) {
|
|
const fileName = String(value || '');
|
|
if (!fileName) {
|
|
return '';
|
|
}
|
|
if (fileName.includes('/') || fileName.includes('\\') || fileName.includes('..')) {
|
|
return '';
|
|
}
|
|
return fileName;
|
|
}
|
|
|
|
function contentDisposition(filename) {
|
|
const fallback = String(filename || 'download')
|
|
.replace(/[\r\n]/g, ' ')
|
|
.replace(/[\\"]/g, '_')
|
|
.replace(/[^ -~]/g, '_');
|
|
const encoded = encodeURIComponent(filename || 'download');
|
|
return `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`;
|
|
}
|
|
|
|
export async function GET(request, { params }) {
|
|
await runCleanupIfNeeded();
|
|
|
|
const resolvedParams = await params;
|
|
const fileName = safeFilename(resolvedParams.filename);
|
|
if (!fileName) {
|
|
return new NextResponse('Ungültiger Dateiname', { status: 400 });
|
|
}
|
|
|
|
const row = await get('SELECT id, original_name, stored_path FROM uploads WHERE stored_name = ?', [fileName]);
|
|
|
|
let filePath;
|
|
let downloadName;
|
|
|
|
if (row) {
|
|
filePath = row.stored_path;
|
|
downloadName = row.original_name || fileName;
|
|
|
|
const requestMeta = await getRequestMeta();
|
|
run('UPDATE uploads SET downloads = downloads + 1 WHERE id = ?', [row.id]).catch(() => undefined);
|
|
logEvent('download', null, { name: fileName, original: downloadName }, requestMeta).catch(() => undefined);
|
|
} else {
|
|
filePath = path.join(shareDir, fileName);
|
|
downloadName = fileName;
|
|
}
|
|
|
|
let fileStat;
|
|
try {
|
|
fileStat = await fs.promises.stat(filePath);
|
|
} catch {
|
|
return new NextResponse('Datei nicht gefunden', { status: 404 });
|
|
}
|
|
|
|
const fileStream = fs.createReadStream(filePath);
|
|
const webStream = Readable.toWeb(fileStream);
|
|
|
|
return new NextResponse(webStream, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Length': String(fileStat.size),
|
|
'Content-Disposition': contentDisposition(downloadName),
|
|
'Cache-Control': 'private, no-store',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
},
|
|
});
|
|
}
|