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', }, }); }