diff --git a/next.config.ts b/next.config.ts index 053acd6..225e495 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,11 +2,6 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'standalone', - experimental: { - serverActions: { - bodySizeLimit: '250mb', - }, - }, }; export default nextConfig; diff --git a/src/app/api/compress-pdf/extreme/route.ts b/src/app/api/compress-pdf/extreme/route.ts index a592c54..675645f 100644 --- a/src/app/api/compress-pdf/extreme/route.ts +++ b/src/app/api/compress-pdf/extreme/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { writeFile, readFile, unlink, mkdir } from "fs/promises"; +import { writeFile, readFile, unlink, mkdir, stat } from "fs/promises"; import { execFile } from "child_process"; import { promisify } from "util"; import { randomUUID } from "crypto"; @@ -79,6 +79,56 @@ function qpdfArgs(input: string, output: string): string[] { ]; } +/** + * Extract the file binary from a raw multipart/form-data buffer. + * Finds the part whose Content-Disposition contains `filename=`, + * then returns the bytes between the header-end and the closing boundary. + */ +function extractFileFromMultipart( + raw: Buffer, + boundary: string, +): Buffer | null { + const boundaryBuf = Buffer.from(`--${boundary}`); + const headerSep = Buffer.from("\r\n\r\n"); + const crlf = Buffer.from("\r\n"); + + let searchFrom = 0; + + while (searchFrom < raw.length) { + const partStart = raw.indexOf(boundaryBuf, searchFrom); + if (partStart === -1) break; + + // Find end of boundary line + const lineEnd = raw.indexOf(crlf, partStart); + if (lineEnd === -1) break; + + // Find blank line separating headers from body + const headerEnd = raw.indexOf(headerSep, lineEnd); + if (headerEnd === -1) break; + + // Check if this part has a filename + const headers = raw.subarray(lineEnd + 2, headerEnd).toString("utf8"); + if (headers.includes("filename=")) { + const fileStart = headerEnd + 4; // skip \r\n\r\n + + // Find closing boundary — search from end to avoid false matches inside PDF + const closingMarker = Buffer.from(`\r\n--${boundary}`); + const fileEnd = raw.lastIndexOf(closingMarker); + + if (fileEnd > fileStart) { + return raw.subarray(fileStart, fileEnd); + } + // Fallback: no closing boundary found, take everything after headers + return raw.subarray(fileStart); + } + + // Skip past this part + searchFrom = headerEnd + 4; + } + + return null; +} + async function cleanup(dir: string) { try { const { readdir } = await import("fs/promises"); @@ -102,30 +152,36 @@ export async function POST(req: NextRequest) { const gsOutputPath = join(tmpDir, "gs-output.pdf"); const finalOutputPath = join(tmpDir, "final.pdf"); - // Use standard formData() — works reliably for large files in Next.js 16 - let pdfBuffer: Buffer; - try { - const formData = await req.formData(); - const fileField = formData.get("fileInput"); - if (!fileField || !(fileField instanceof Blob)) { - return NextResponse.json( - { error: "Lipsește fișierul PDF." }, - { status: 400 }, - ); - } - pdfBuffer = Buffer.from(await fileField.arrayBuffer()); - } catch (parseErr) { - const msg = - parseErr instanceof Error ? parseErr.message : "Parse error"; + // Collect raw body via arrayBuffer() — more reliable than formData() for + // large files, and more reliable than Readable.fromWeb streaming to disk. + if (!req.body) { return NextResponse.json( - { error: `Nu s-a putut citi fișierul: ${msg}` }, + { error: "Lipsește fișierul PDF." }, { status: 400 }, ); } - if (pdfBuffer.length < 100) { + const rawBuf = Buffer.from(await req.arrayBuffer()); + + // Extract PDF from multipart body + const contentType = req.headers.get("content-type") || ""; + const boundaryMatch = contentType.match( + /boundary=(?:"([^"]+)"|([^\s;]+))/, + ); + const boundary = boundaryMatch?.[1] ?? boundaryMatch?.[2] ?? ""; + + if (!boundary) { return NextResponse.json( - { error: "Fișierul PDF este gol sau prea mic." }, + { error: "Invalid request — missing multipart boundary." }, + { status: 400 }, + ); + } + + const pdfBuffer = extractFileFromMultipart(rawBuf, boundary); + + if (!pdfBuffer || pdfBuffer.length < 100) { + return NextResponse.json( + { error: "Fișierul PDF este gol sau nu a putut fi extras." }, { status: 400 }, ); } @@ -174,9 +230,7 @@ export async function POST(req: NextRequest) { // Verify GS output is a valid non-empty PDF let gsSize = 0; try { - const gsStat = await import("fs/promises").then((fs) => - fs.stat(gsOutputPath), - ); + const gsStat = await stat(gsOutputPath); gsSize = gsStat.size; } catch { return NextResponse.json( diff --git a/src/app/api/compress-pdf/route.ts b/src/app/api/compress-pdf/route.ts index 55c00ba..8f69363 100644 --- a/src/app/api/compress-pdf/route.ts +++ b/src/app/api/compress-pdf/route.ts @@ -7,41 +7,22 @@ const STIRLING_PDF_API_KEY = export async function POST(req: NextRequest) { try { - // Parse incoming form data — get file + optimizeLevel - let formData: FormData; - try { - formData = await req.formData(); - } catch (parseErr) { - const msg = - parseErr instanceof Error ? parseErr.message : "Parse error"; - return NextResponse.json( - { error: `Nu s-a putut citi formularul: ${msg}` }, - { status: 400 }, - ); - } + // Buffer the full body then forward to Stirling — streaming passthrough + // (req.body + duplex:half) is unreliable for large files in Next.js. + const bodyBytes = await req.arrayBuffer(); + const contentType = req.headers.get("content-type") || ""; - const fileField = formData.get("fileInput"); - if (!fileField || !(fileField instanceof Blob)) { - return NextResponse.json( - { error: "Lipsește fișierul PDF." }, - { status: 400 }, - ); - } - - const optimizeLevel = formData.get("optimizeLevel") ?? "3"; - const originalSize = fileField.size; - - // Build fresh FormData for Stirling - const stirlingForm = new FormData(); - stirlingForm.append("fileInput", fileField, "input.pdf"); - stirlingForm.append("optimizeLevel", String(optimizeLevel)); + // Extract original file size from the multipart body for the response header + // (rough estimate — the overhead of multipart framing is negligible for large PDFs) + const originalSize = bodyBytes.byteLength; const res = await fetch(`${STIRLING_PDF_URL}/api/v1/misc/compress-pdf`, { method: "POST", headers: { "X-API-KEY": STIRLING_PDF_API_KEY, + "Content-Type": contentType, }, - body: stirlingForm, + body: bodyBytes, signal: AbortSignal.timeout(300_000), // 5 min for large files }); @@ -56,7 +37,7 @@ export async function POST(req: NextRequest) { const blob = await res.blob(); const buffer = Buffer.from(await blob.arrayBuffer()); - return new NextResponse(buffer, { + return new NextResponse(new Uint8Array(buffer), { status: 200, headers: { "Content-Type": "application/pdf",