fix(sync): ApiResponse envelope + atomic DB wipe on restore
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { spawnSync, execSync } from 'child_process';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
@@ -14,12 +15,12 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => ({} as Record<string, unknown>));
|
||||
const expected = process.env.PULSE_SYNC_TOKEN;
|
||||
if (!expected) {
|
||||
return NextResponse.json({ error: 'Sync not configured (PULSE_SYNC_TOKEN missing)' }, { status: 500 });
|
||||
return NextResponse.json(err('Sync not configured (PULSE_SYNC_TOKEN missing)'), { status: 500 });
|
||||
}
|
||||
const prodUrl = process.env.PULSE_PROD_URL ?? 'https://pulsy.hellobaka.com';
|
||||
const confirm = (body as Record<string, unknown>).confirm;
|
||||
if (confirm !== true && confirm !== 'true') {
|
||||
return NextResponse.json({ error: 'Missing confirm:true — this wipes test DB' }, { status: 400 });
|
||||
return NextResponse.json(err('Missing confirm:true — this wipes test DB'), { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch dump from prod
|
||||
@@ -31,44 +32,43 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
return NextResponse.json({ error: `prod dump failed: ${res.status} ${text.slice(0, 800)}` }, { status: 502 });
|
||||
return NextResponse.json(err(`prod dump failed: ${res.status} ${text.slice(0, 800)}`), { status: 502 });
|
||||
}
|
||||
dump = await res.text();
|
||||
if (!dump || dump.length < 100) {
|
||||
return NextResponse.json({ error: 'prod dump empty or too small', len: dump.length }, { status: 502 });
|
||||
return NextResponse.json(err('prod dump empty or too small'), { status: 502 });
|
||||
}
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: `fetch prod failed: ${(e as Error).message}` }, { status: 502 });
|
||||
return NextResponse.json(err(`fetch prod failed: ${(e as Error).message}`), { status: 502 });
|
||||
}
|
||||
|
||||
const dbPath = resolveDbPath();
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// Write dump to temp file then restore
|
||||
const tmp = path.join(os.tmpdir(), `pulse-restore-${Date.now()}.sql`);
|
||||
try {
|
||||
fs.writeFileSync(tmp, dump, 'utf-8');
|
||||
|
||||
// Count tables in dump for sanity
|
||||
const tableCount = (dump.match(/CREATE TABLE/g) || []).length;
|
||||
|
||||
// Restore: sqlite3 db < dump.sql
|
||||
// Wipe existing DB so PKs don't collide on re-sync (atomic replace)
|
||||
for (const suffix of ['', '-wal', '-shm']) {
|
||||
try { fs.unlinkSync(dbPath + suffix); } catch {}
|
||||
}
|
||||
const restore = spawnSync('sh', ['-c', `sqlite3 "${dbPath}" < "${tmp}"`], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
||||
if (restore.status !== 0) {
|
||||
return NextResponse.json({ error: 'restore failed', detail: (restore.stderr || restore.stdout || '').slice(0, 3000) }, { status: 500 });
|
||||
return NextResponse.json(err('restore failed: ' + (restore.stderr || restore.stdout || '').slice(0, 1500)), { status: 500 });
|
||||
}
|
||||
|
||||
// Quick sanity: count rows
|
||||
const check = spawnSync('sqlite3', [dbPath, 'SELECT "companies:" || (SELECT count(*) FROM companies) || " employees:" || (SELECT count(*) FROM employees) || " entries:" || (SELECT count(*) FROM time_entries);'], { encoding: 'utf-8' });
|
||||
const summary = (check.stdout || '').trim();
|
||||
|
||||
return NextResponse.json({ ok: true, bytes: dump.length, tables: tableCount, summary, dbPath });
|
||||
return NextResponse.json(ok({ bytes: dump.length, tables: tableCount, summary, dbPath }));
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmp); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ ok: true, hint: 'POST with { token, confirm: true } and x-sync-token header' });
|
||||
return NextResponse.json(ok({ hint: 'POST with { confirm: true }' }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user