Files
files/nextjs/app/manage/api/upload/route.js
2026-04-11 09:27:20 +02:00

187 lines
4.8 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
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 { streamMultipartToPath } from '@/src/lib/multipart.js';
import { safeBaseName } from '@/src/lib/files.js';
import { getAuthenticatedUser, getRequestMeta, verifyCsrfToken } 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));
}
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);
}
const now = Date.now();
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);
}
let parsed;
try {
parsed = await streamMultipartToPath(request, {
fileFieldName: 'file',
targetPath: storedPath,
maxFileBytes: maxUploadBytes,
});
} catch (error) {
if (error && error.message === 'file-too-large') {
return errorResponse(request, `Datei überschreitet das Größenlimit (${maxUploadBytes} Bytes).`, 413);
}
if (error && error.message === 'missing-file') {
return errorResponse(request, 'Keine Datei hochgeladen.', 400);
}
return errorResponse(request, 'Upload fehlgeschlagen.', 500);
}
try {
await verifyCsrfToken(parsed.fields.csrfToken);
} catch {
await fs.promises.rm(storedPath, { force: true }).catch(() => undefined);
return errorResponse(request, 'CSRF-Prüfung fehlgeschlagen.', 403);
}
const retentionSeconds = parseHours(parsed.fields.retentionHours, uploadTtlSeconds);
const cappedRetention = Math.min(retentionSeconds, maxRetentionSeconds);
const originalName = safeBaseName(parsed.file.filename, 'upload');
try {
const sizeBytes = parsed.file.sizeBytes;
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: sizeBytes },
await getRequestMeta()
);
} catch {
await fs.promises.rm(storedPath, { force: true }).catch(() => undefined);
return errorResponse(request, 'Upload fehlgeschlagen.', 500);
}
return successResponse(request, 'Upload abgeschlossen.');
}