(hopefully) fixed upload for large files

This commit is contained in:
Ludwig Lehnert
2026-04-11 09:27:20 +02:00
parent d86139f574
commit 1adecba896
6 changed files with 203 additions and 122 deletions

View File

@@ -1,9 +1,6 @@
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 {
@@ -14,8 +11,9 @@ import {
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, verifyCsrf } from '@/src/lib/security.js';
import { getAuthenticatedUser, getRequestMeta, verifyCsrfToken } from '@/src/lib/security.js';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
@@ -55,39 +53,6 @@ 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)) {
@@ -140,32 +105,7 @@ export async function POST(request) {
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 = '';
@@ -186,8 +126,36 @@ export async function POST(request) {
return errorResponse(request, 'Upload-ID konnte nicht erzeugt werden.', 500);
}
let parsed;
try {
const sizeBytes = await writeUploadedFile(uploadedFile, storedPath);
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)
@@ -206,10 +174,11 @@ export async function POST(request) {
await logEvent(
'upload',
user.username,
{ name: storedName, size: Number(uploadedFile.size || 0) },
{ name: storedName, size: sizeBytes },
await getRequestMeta()
);
} catch {
await fs.promises.rm(storedPath, { force: true }).catch(() => undefined);
return errorResponse(request, 'Upload fehlgeschlagen.', 500);
}