import { NextRequest, NextResponse } from "next/server"; /** * iLovePDF API integration for PDF compression. * * Workflow: auth → start → upload → process → download * Docs: https://www.iloveapi.com/docs/api-reference * * Env vars: ILOVEPDF_PUBLIC_KEY, ILOVEPDF_SECRET_KEY * Free tier: 250 files/month */ const ILOVEPDF_PUBLIC_KEY = process.env.ILOVEPDF_PUBLIC_KEY ?? ""; const API_BASE = "https://api.ilovepdf.com/v1"; /** * Extract the file binary from a raw multipart/form-data buffer. */ function extractFileFromMultipart( raw: Buffer, boundary: string, ): { buffer: Buffer; filename: string } | 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; const lineEnd = raw.indexOf(crlf, partStart); if (lineEnd === -1) break; const headerEnd = raw.indexOf(headerSep, lineEnd); if (headerEnd === -1) break; const headers = raw.subarray(lineEnd + 2, headerEnd).toString("utf8"); if (headers.includes("filename=")) { const fileStart = headerEnd + 4; // Extract original filename const fnMatch = headers.match(/filename="([^"]+)"/); const filename = fnMatch?.[1] ?? "input.pdf"; const closingMarker = Buffer.from(`\r\n--${boundary}`); const fileEnd = raw.lastIndexOf(closingMarker); const buffer = fileEnd > fileStart ? raw.subarray(fileStart, fileEnd) : raw.subarray(fileStart); return { buffer, filename }; } searchFrom = headerEnd + 4; } return null; } /** * Extract a text field value from multipart body. */ function extractFieldFromMultipart( raw: Buffer, boundary: string, fieldName: string, ): string | null { const boundaryBuf = Buffer.from(`--${boundary}`); const headerSep = Buffer.from("\r\n\r\n"); const crlf = Buffer.from("\r\n"); const namePattern = `name="${fieldName}"`; let searchFrom = 0; while (searchFrom < raw.length) { const partStart = raw.indexOf(boundaryBuf, searchFrom); if (partStart === -1) break; const lineEnd = raw.indexOf(crlf, partStart); if (lineEnd === -1) break; const headerEnd = raw.indexOf(headerSep, lineEnd); if (headerEnd === -1) break; const headers = raw.subarray(lineEnd + 2, headerEnd).toString("utf8"); if (headers.includes(namePattern) && !headers.includes("filename=")) { const valueStart = headerEnd + 4; const nextBoundary = raw.indexOf( Buffer.from(`\r\n--${boundary}`), valueStart, ); if (nextBoundary > valueStart) { return raw.subarray(valueStart, nextBoundary).toString("utf8").trim(); } } searchFrom = headerEnd + 4; } return null; } export async function POST(req: NextRequest) { if (!ILOVEPDF_PUBLIC_KEY) { return NextResponse.json( { error: "iLovePDF nu este configurat. Setează ILOVEPDF_PUBLIC_KEY în variabilele de mediu.", }, { status: 501 }, ); } try { // Parse multipart body if (!req.body) { return NextResponse.json( { error: "Lipsește fișierul PDF." }, { status: 400 }, ); } const rawBuf = Buffer.from(await req.arrayBuffer()); 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: "Invalid request." }, { status: 400 }, ); } const fileData = extractFileFromMultipart(rawBuf, boundary); if (!fileData || fileData.buffer.length < 100) { return NextResponse.json( { error: "Fișierul PDF nu a putut fi extras." }, { status: 400 }, ); } // Extract compression level (extreme / recommended / low) const levelParam = extractFieldFromMultipart(rawBuf, boundary, "level"); const compressionLevel = levelParam === "extreme" ? "extreme" : levelParam === "low" ? "low" : "recommended"; const originalSize = fileData.buffer.length; // Step 1: Authenticate const authRes = await fetch(`${API_BASE}/auth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ public_key: ILOVEPDF_PUBLIC_KEY }), }); if (!authRes.ok) { const text = await authRes.text().catch(() => ""); return NextResponse.json( { error: `iLovePDF auth failed: ${authRes.status} — ${text}` }, { status: 502 }, ); } const { token } = (await authRes.json()) as { token: string }; // Step 2: Start compress task const startRes = await fetch(`${API_BASE}/start/compress`, { method: "GET", headers: { Authorization: `Bearer ${token}` }, }); if (!startRes.ok) { const text = await startRes.text().catch(() => ""); return NextResponse.json( { error: `iLovePDF start failed: ${startRes.status} — ${text}` }, { status: 502 }, ); } const { server, task } = (await startRes.json()) as { server: string; task: string; }; // Step 3: Upload file const uploadForm = new FormData(); uploadForm.append("task", task); uploadForm.append( "file", new Blob([new Uint8Array(fileData.buffer)], { type: "application/pdf" }), fileData.filename, ); const uploadRes = await fetch(`https://${server}/v1/upload`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: uploadForm, signal: AbortSignal.timeout(300_000), // 5 min for large files }); if (!uploadRes.ok) { const text = await uploadRes.text().catch(() => ""); return NextResponse.json( { error: `iLovePDF upload failed: ${uploadRes.status} — ${text}` }, { status: 502 }, ); } const { server_filename } = (await uploadRes.json()) as { server_filename: string; }; // Step 4: Process const processRes = await fetch(`https://${server}/v1/process`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ task, tool: "compress", compression_level: compressionLevel, files: [ { server_filename, filename: fileData.filename, }, ], }), signal: AbortSignal.timeout(300_000), }); if (!processRes.ok) { const text = await processRes.text().catch(() => ""); return NextResponse.json( { error: `iLovePDF process failed: ${processRes.status} — ${text}` }, { status: 502 }, ); } // Step 5: Download result const downloadRes = await fetch( `https://${server}/v1/download/${task}`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(300_000), }, ); if (!downloadRes.ok) { const text = await downloadRes.text().catch(() => ""); return NextResponse.json( { error: `iLovePDF download failed: ${downloadRes.status} — ${text}`, }, { status: 502 }, ); } const resultBlob = await downloadRes.blob(); const resultBuffer = Buffer.from(await resultBlob.arrayBuffer()); const compressedSize = resultBuffer.length; // Clean up task on iLovePDF fetch(`https://${server}/v1/task/${task}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }).catch(() => {}); return new NextResponse(new Uint8Array(resultBuffer), { status: 200, headers: { "Content-Type": "application/pdf", "Content-Disposition": `attachment; filename="${fileData.filename.replace(/\.pdf$/i, "-comprimat.pdf")}"`, "X-Original-Size": String(originalSize), "X-Compressed-Size": String(compressedSize), }, }); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; return NextResponse.json( { error: `Eroare iLovePDF: ${message}` }, { status: 500 }, ); } }