No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

60 líneas
1.9 KiB

  1. import { NextResponse } from 'next/server';
  2. import { spawn } from 'node:child_process';
  3. import path from 'node:path';
  4. import { checkSystemBin } from '@/lib/system-bins';
  5. export const dynamic = 'force-dynamic';
  6. export const maxDuration = 600;
  7. const DATA_DIR = path.join(process.cwd(), 'data');
  8. function timestamp(): string {
  9. const d = new Date();
  10. const pad = (n: number) => String(n).padStart(2, '0');
  11. return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
  12. }
  13. export async function GET() {
  14. if (!(await checkSystemBin('zip', '-v'))) {
  15. return NextResponse.json({ error: "Binario 'zip' non disponibile sul server." }, { status: 503 });
  16. }
  17. // -r ricorsivo, - stdout, -x esclude pattern relativi al cwd.
  18. // Stato runtime escluso: .tmp/, transcode-jobs.json.
  19. const child = spawn(
  20. 'zip',
  21. [
  22. '-r', '-',
  23. 'cards.txt', 'portals.txt', 'uploads', 'fonts',
  24. '-x', 'uploads/.tmp/*',
  25. ],
  26. { cwd: DATA_DIR },
  27. );
  28. // Adatta stdout di Node a Web ReadableStream per la Response di Next.
  29. const stream = new ReadableStream<Uint8Array>({
  30. start(controller) {
  31. child.stdout.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk)));
  32. child.stdout.on('end', () => controller.close());
  33. child.on('error', (err) => controller.error(err));
  34. child.on('exit', code => {
  35. if (code !== 0 && code !== null) {
  36. // 12 = "nothing to do" (cartella vuota). Tutto il resto è errore vero.
  37. if (code !== 12) controller.error(new Error(`zip exited with code ${code}`));
  38. }
  39. });
  40. },
  41. cancel() {
  42. try { child.kill('SIGTERM'); } catch { /* ignore */ }
  43. },
  44. });
  45. return new Response(stream, {
  46. headers: {
  47. 'Content-Type': 'application/zip',
  48. 'Content-Disposition': `attachment; filename="interceptor-backup-${timestamp()}.zip"`,
  49. 'Cache-Control': 'no-store',
  50. },
  51. });
  52. }