import { NextResponse } from 'next/server'; import { spawn } from 'node:child_process'; import { mkdir, writeFile, readFile, rename, rm } from 'node:fs/promises'; import path from 'node:path'; import crypto from 'node:crypto'; import { checkSystemBin } from '@/lib/system-bins'; export const dynamic = 'force-dynamic'; export const maxDuration = 600; const PROJECT_ROOT = process.cwd(); const DATA_DIR = path.join(PROJECT_ROOT, 'data'); const RESTORE_STAGING = path.join(PROJECT_ROOT, '.restore-staging'); function runUnzip(zipPath: string, destDir: string): Promise<{ code: number; stderr: string }> { return new Promise(resolve => { const child = spawn('unzip', ['-o', '-q', zipPath, '-d', destDir]); let stderr = ''; child.stderr.on('data', d => { stderr += d.toString(); }); child.on('error', () => resolve({ code: -1, stderr: stderr || 'unzip non avviato' })); child.on('exit', code => resolve({ code: code ?? -1, stderr })); }); } export async function POST(request: Request) { if (!(await checkSystemBin('unzip', '-v'))) { return NextResponse.json({ error: "Binario 'unzip' non disponibile sul server." }, { status: 503 }); } const sessionId = crypto.randomUUID(); const sessionDir = path.join(RESTORE_STAGING, sessionId); const zipPath = path.join(sessionDir, 'backup.zip'); const extractDir = path.join(sessionDir, 'extract'); try { const formData = await request.formData(); const file = formData.get('file') as File | null; if (!file) { return NextResponse.json({ error: 'Nessun file ricevuto.' }, { status: 400 }); } await mkdir(extractDir, { recursive: true }); const buf = Buffer.from(await file.arrayBuffer()); await writeFile(zipPath, buf); const { code, stderr } = await runUnzip(zipPath, extractDir); if (code !== 0) { return NextResponse.json( { error: 'Impossibile estrarre lo ZIP.', detail: stderr.split('\n').slice(0, 3).join(' ') }, { status: 400 }, ); } // Validazione struttura: cards.txt e portals.txt devono esistere E essere JSON validi. let cardsCount = 0; let portalsCount = 0; try { const cardsRaw = await readFile(path.join(extractDir, 'cards.txt'), 'utf-8'); const cardsParsed = JSON.parse(cardsRaw || '[]'); if (!Array.isArray(cardsParsed)) throw new Error('cards.txt non è un array JSON'); cardsCount = cardsParsed.length; } catch (e) { return NextResponse.json( { error: `Backup non valido: cards.txt assente o malformato (${(e as Error).message}).` }, { status: 400 }, ); } try { const portalsRaw = await readFile(path.join(extractDir, 'portals.txt'), 'utf-8'); const portalsParsed = JSON.parse(portalsRaw || '[]'); if (!Array.isArray(portalsParsed)) throw new Error('portals.txt non è un array JSON'); portalsCount = portalsParsed.length; } catch (e) { return NextResponse.json( { error: `Backup non valido: portals.txt assente o malformato (${(e as Error).message}).` }, { status: 400 }, ); } // Atomic swap: data → data.bak-, extract → data. const ts = Date.now(); const backupOldDir = path.join(PROJECT_ROOT, `data.bak-${ts}`); try { await rename(DATA_DIR, backupOldDir); } catch (err) { const e = err as NodeJS.ErrnoException; if (e.code !== 'ENOENT') throw err; // data/ non esisteva: ok, andiamo avanti } try { await rename(extractDir, DATA_DIR); } catch (err) { // Revert: rimettiamo data/ al suo posto. try { await rename(backupOldDir, DATA_DIR); } catch { /* ignore */ } throw err; } // Cleanup zip + cartella staging della sessione (l'extract è già stato spostato). try { await rm(sessionDir, { recursive: true, force: true }); } catch { /* ignore */ } return NextResponse.json({ ok: true, restored: { cards: cardsCount, portals: portalsCount }, previousDataBackup: path.basename(backupOldDir), }); } catch (error) { console.error('Restore error:', error); // Cleanup staging in caso di errore non gestito sopra. try { await rm(sessionDir, { recursive: true, force: true }); } catch { /* ignore */ } return NextResponse.json({ error: 'Errore durante il ripristino.' }, { status: 500 }); } }