Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

63 linhas
2.0 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(
  16. { error: "Cannot create the backup: the 'zip' command is not installed on the server. Ask the administrator to install it." },
  17. { status: 503 },
  18. );
  19. }
  20. // -r ricorsivo, - stdout, -x esclude pattern relativi al cwd.
  21. // Stato runtime escluso: .tmp/, transcode-jobs.json.
  22. const child = spawn(
  23. 'zip',
  24. [
  25. '-r', '-',
  26. 'cards.txt', 'portals.txt', 'uploads', 'fonts',
  27. '-x', 'uploads/.tmp/*',
  28. ],
  29. { cwd: DATA_DIR },
  30. );
  31. // Adatta stdout di Node a Web ReadableStream per la Response di Next.
  32. const stream = new ReadableStream<Uint8Array>({
  33. start(controller) {
  34. child.stdout.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk)));
  35. child.stdout.on('end', () => controller.close());
  36. child.on('error', (err) => controller.error(err));
  37. child.on('exit', code => {
  38. if (code !== 0 && code !== null) {
  39. // 12 = "nothing to do" (cartella vuota). Tutto il resto è errore vero.
  40. if (code !== 12) controller.error(new Error(`zip exited with code ${code}`));
  41. }
  42. });
  43. },
  44. cancel() {
  45. try { child.kill('SIGTERM'); } catch { /* ignore */ }
  46. },
  47. });
  48. return new Response(stream, {
  49. headers: {
  50. 'Content-Type': 'application/zip',
  51. 'Content-Disposition': `attachment; filename="interceptor-backup-${timestamp()}.zip"`,
  52. 'Cache-Control': 'no-store',
  53. },
  54. });
  55. }