Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

113 lignes
4.3 KiB

  1. import { NextResponse } from 'next/server';
  2. import { spawn } from 'node:child_process';
  3. import { mkdir, writeFile, readFile, rename, rm } from 'node:fs/promises';
  4. import path from 'node:path';
  5. import crypto from 'node:crypto';
  6. import { checkSystemBin } from '@/lib/system-bins';
  7. export const dynamic = 'force-dynamic';
  8. export const maxDuration = 600;
  9. const PROJECT_ROOT = process.cwd();
  10. const DATA_DIR = path.join(PROJECT_ROOT, 'data');
  11. const RESTORE_STAGING = path.join(PROJECT_ROOT, '.restore-staging');
  12. function runUnzip(zipPath: string, destDir: string): Promise<{ code: number; stderr: string }> {
  13. return new Promise(resolve => {
  14. const child = spawn('unzip', ['-o', '-q', zipPath, '-d', destDir]);
  15. let stderr = '';
  16. child.stderr.on('data', d => { stderr += d.toString(); });
  17. child.on('error', () => resolve({ code: -1, stderr: stderr || 'unzip non avviato' }));
  18. child.on('exit', code => resolve({ code: code ?? -1, stderr }));
  19. });
  20. }
  21. export async function POST(request: Request) {
  22. if (!(await checkSystemBin('unzip', '-v'))) {
  23. return NextResponse.json({ error: "Binario 'unzip' non disponibile sul server." }, { status: 503 });
  24. }
  25. const sessionId = crypto.randomUUID();
  26. const sessionDir = path.join(RESTORE_STAGING, sessionId);
  27. const zipPath = path.join(sessionDir, 'backup.zip');
  28. const extractDir = path.join(sessionDir, 'extract');
  29. try {
  30. const formData = await request.formData();
  31. const file = formData.get('file') as File | null;
  32. if (!file) {
  33. return NextResponse.json({ error: 'Nessun file ricevuto.' }, { status: 400 });
  34. }
  35. await mkdir(extractDir, { recursive: true });
  36. const buf = Buffer.from(await file.arrayBuffer());
  37. await writeFile(zipPath, buf);
  38. const { code, stderr } = await runUnzip(zipPath, extractDir);
  39. if (code !== 0) {
  40. return NextResponse.json(
  41. { error: 'Impossibile estrarre lo ZIP.', detail: stderr.split('\n').slice(0, 3).join(' ') },
  42. { status: 400 },
  43. );
  44. }
  45. // Validazione struttura: cards.txt e portals.txt devono esistere E essere JSON validi.
  46. let cardsCount = 0;
  47. let portalsCount = 0;
  48. try {
  49. const cardsRaw = await readFile(path.join(extractDir, 'cards.txt'), 'utf-8');
  50. const cardsParsed = JSON.parse(cardsRaw || '[]');
  51. if (!Array.isArray(cardsParsed)) throw new Error('cards.txt non è un array JSON');
  52. cardsCount = cardsParsed.length;
  53. } catch (e) {
  54. return NextResponse.json(
  55. { error: `Backup non valido: cards.txt assente o malformato (${(e as Error).message}).` },
  56. { status: 400 },
  57. );
  58. }
  59. try {
  60. const portalsRaw = await readFile(path.join(extractDir, 'portals.txt'), 'utf-8');
  61. const portalsParsed = JSON.parse(portalsRaw || '[]');
  62. if (!Array.isArray(portalsParsed)) throw new Error('portals.txt non è un array JSON');
  63. portalsCount = portalsParsed.length;
  64. } catch (e) {
  65. return NextResponse.json(
  66. { error: `Backup non valido: portals.txt assente o malformato (${(e as Error).message}).` },
  67. { status: 400 },
  68. );
  69. }
  70. // Atomic swap: data → data.bak-<ts>, extract → data.
  71. const ts = Date.now();
  72. const backupOldDir = path.join(PROJECT_ROOT, `data.bak-${ts}`);
  73. try {
  74. await rename(DATA_DIR, backupOldDir);
  75. } catch (err) {
  76. const e = err as NodeJS.ErrnoException;
  77. if (e.code !== 'ENOENT') throw err;
  78. // data/ non esisteva: ok, andiamo avanti
  79. }
  80. try {
  81. await rename(extractDir, DATA_DIR);
  82. } catch (err) {
  83. // Revert: rimettiamo data/ al suo posto.
  84. try { await rename(backupOldDir, DATA_DIR); } catch { /* ignore */ }
  85. throw err;
  86. }
  87. // Cleanup zip + cartella staging della sessione (l'extract è già stato spostato).
  88. try { await rm(sessionDir, { recursive: true, force: true }); } catch { /* ignore */ }
  89. return NextResponse.json({
  90. ok: true,
  91. restored: { cards: cardsCount, portals: portalsCount },
  92. previousDataBackup: path.basename(backupOldDir),
  93. });
  94. } catch (error) {
  95. console.error('Restore error:', error);
  96. // Cleanup staging in caso di errore non gestito sopra.
  97. try { await rm(sessionDir, { recursive: true, force: true }); } catch { /* ignore */ }
  98. return NextResponse.json({ error: 'Errore durante il ripristino.' }, { status: 500 });
  99. }
  100. }