import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { NextResponse } from 'next/server'; import { managementBasePath, maxRetentionSeconds, maxUploadBytes, shareDir, uploadTtlSeconds, } from '@/src/lib/config.js'; import { logEvent, run, runCleanupIfNeeded } from '@/src/lib/db.js'; import { safeBaseName } from '@/src/lib/files.js'; import { getAuthenticatedUser, getRequestMeta, verifyCsrf } from '@/src/lib/security.js'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; function parseHours(value, fallbackSeconds) { const parsed = Number.parseFloat(String(value || '')); if (Number.isFinite(parsed) && parsed > 0) { return Math.round(parsed * 3600); } return fallbackSeconds; } function toBase32(buffer) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; let output = ''; for (const byte of buffer) { value = (value << 8) | byte; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } return output; } function createRandomId() { return toBase32(crypto.randomBytes(5)); } async function writeUploadedFile(uploadedFile, targetPath) { try { if (typeof uploadedFile.stream === 'function') { await pipeline(Readable.fromWeb(uploadedFile.stream()), fs.createWriteStream(targetPath)); } else { const buffer = Buffer.from(await uploadedFile.arrayBuffer()); await fs.promises.writeFile(targetPath, buffer); } } catch (error) { await fs.promises.rm(targetPath, { force: true }).catch(() => undefined); throw error; } const declaredSize = Number(uploadedFile.size || 0); if (Number.isFinite(declaredSize) && declaredSize >= 0) { return declaredSize; } const stat = await fs.promises.stat(targetPath); return stat.size; } function uploadedFileFromForm(formData, fieldName = 'file') { const candidate = formData.get(fieldName); if (!candidate || typeof candidate === 'string') { return null; } if (typeof candidate.arrayBuffer !== 'function') { return null; } return candidate; } function dashboardHref(params = {}) { const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value) { query.set(key, String(value)); } } const serialized = query.toString(); return serialized ? `${managementBasePath}/dashboard?${serialized}` : `${managementBasePath}/dashboard`; } function expectsHtml(request) { return String(request.headers.get('accept') || '').includes('text/html'); } function errorResponse(request, message, status = 400) { if (expectsHtml(request)) { return new NextResponse(null, { status: 303, headers: { location: dashboardHref({ error: message }), }, }); } return NextResponse.json({ ok: false, error: message }, { status }); } function successResponse(request, message) { const redirectPath = dashboardHref({ success: message }); if (expectsHtml(request)) { return new NextResponse(null, { status: 303, headers: { location: redirectPath, }, }); } return NextResponse.json({ ok: true, redirect: redirectPath }); } export async function POST(request) { await runCleanupIfNeeded(); const user = await getAuthenticatedUser(); if (!user) { return errorResponse(request, 'Nicht angemeldet.', 401); } let formData; try { formData = await request.formData(); } catch { return errorResponse(request, 'Ungültige Formulardaten.', 400); } try { await verifyCsrf(formData); } catch { return errorResponse(request, 'CSRF-Prüfung fehlgeschlagen.', 403); } const uploadedFile = uploadedFileFromForm(formData, 'file'); if (!uploadedFile || Number(uploadedFile.size || 0) <= 0) { return errorResponse(request, 'Keine Datei hochgeladen.', 400); } if (maxUploadBytes > 0 && Number(uploadedFile.size || 0) > maxUploadBytes) { return errorResponse(request, `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`, 413); } const now = Date.now(); const originalName = safeBaseName(uploadedFile.name, 'upload'); const retentionSeconds = parseHours(formData.get('retentionHours'), uploadTtlSeconds); const cappedRetention = Math.min(retentionSeconds, maxRetentionSeconds); let storedName = ''; let storedPath = ''; for (let attempts = 0; attempts < 5; attempts += 1) { const candidate = createRandomId(); const candidatePath = path.join(shareDir, candidate); try { await fs.promises.access(candidatePath, fs.constants.F_OK); } catch { storedName = candidate; storedPath = candidatePath; break; } } if (!storedName || !storedPath) { return errorResponse(request, 'Upload-ID konnte nicht erzeugt werden.', 500); } try { const sizeBytes = await writeUploadedFile(uploadedFile, storedPath); await run( `INSERT INTO uploads (owner, original_name, stored_name, stored_path, size_bytes, uploaded_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ user.username, originalName, storedName, storedPath, sizeBytes, now, now + cappedRetention * 1000, ] ); await logEvent( 'upload', user.username, { name: storedName, size: Number(uploadedFile.size || 0) }, await getRequestMeta() ); } catch { return errorResponse(request, 'Upload fehlgeschlagen.', 500); } return successResponse(request, 'Upload abgeschlossen.'); }