import { NextResponse } from 'next/server'; import { spawn } from 'node:child_process'; import path from 'node:path'; import { checkSystemBin } from '@/lib/system-bins'; export const dynamic = 'force-dynamic'; export const maxDuration = 600; const DATA_DIR = path.join(process.cwd(), 'data'); function timestamp(): string { const d = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; } export async function GET() { if (!(await checkSystemBin('zip', '-v'))) { return NextResponse.json({ error: "Binario 'zip' non disponibile sul server." }, { status: 503 }); } // -r ricorsivo, - stdout, -x esclude pattern relativi al cwd. // Stato runtime escluso: .tmp/, transcode-jobs.json. const child = spawn( 'zip', [ '-r', '-', 'cards.txt', 'portals.txt', 'uploads', 'fonts', '-x', 'uploads/.tmp/*', ], { cwd: DATA_DIR }, ); // Adatta stdout di Node a Web ReadableStream per la Response di Next. const stream = new ReadableStream({ start(controller) { child.stdout.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk))); child.stdout.on('end', () => controller.close()); child.on('error', (err) => controller.error(err)); child.on('exit', code => { if (code !== 0 && code !== null) { // 12 = "nothing to do" (cartella vuota). Tutto il resto รจ errore vero. if (code !== 12) controller.error(new Error(`zip exited with code ${code}`)); } }); }, cancel() { try { child.kill('SIGTERM'); } catch { /* ignore */ } }, }); return new Response(stream, { headers: { 'Content-Type': 'application/zip', 'Content-Disposition': `attachment; filename="interceptop-backup-${timestamp()}.zip"`, 'Cache-Control': 'no-store', }, }); }