From 1484d051a17dec658e25a662482e579aafd7974e Mon Sep 17 00:00:00 2001 From: Lukas Davidovic Date: Tue, 4 Feb 2025 09:51:26 +0100 Subject: [PATCH] data export and backup utilities --- lib/backup-utils.ts | 49 +++++++++++++++++++++++ pages/api/admin/export.ts | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 lib/backup-utils.ts create mode 100644 pages/api/admin/export.ts diff --git a/lib/backup-utils.ts b/lib/backup-utils.ts new file mode 100644 index 0000000..67eb413 --- /dev/null +++ b/lib/backup-utils.ts @@ -0,0 +1,49 @@ +import { exec } from 'child_process' +import path from 'path' +import { promisify } from 'util' + +const execAsync = promisify(exec) + +export async function createDatabaseBackup(): Promise { + const dbPath = path.join(process.cwd(), 'database', 'antihoax.db') + const backupPath = path.join(process.cwd(), 'backups', `backup_${Date.now()}.db`) + + try { + // Ensure backup directory exists + await execAsync('mkdir -p backups') + + // Create SQLite backup using .backup command + await execAsync(`sqlite3 "${dbPath}" ".backup '${backupPath}'"`) + + return backupPath + } catch (error) { + console.error('Backup failed:', error) + throw new Error('Database backup failed') + } +} + +export async function restoreDatabase(backupPath: string): Promise { + const dbPath = path.join(process.cwd(), 'database', 'antihoax.db') + + try { + // Create backup of current DB first + await createDatabaseBackup() + + // Copy backup to main location + await execAsync(`cp "${backupPath}" "${dbPath}"`) + + console.log('Database restored successfully') + } catch (error) { + console.error('Restore failed:', error) + throw new Error('Database restore failed') + } +} + +export async function listBackups(): Promise { + try { + const { stdout } = await execAsync('ls -la backups/*.db 2>/dev/null || true') + return stdout.trim() ? stdout.trim().split('\n') : [] + } catch { + return [] + } +} \ No newline at end of file diff --git a/pages/api/admin/export.ts b/pages/api/admin/export.ts new file mode 100644 index 0000000..828f74c --- /dev/null +++ b/pages/api/admin/export.ts @@ -0,0 +1,84 @@ +import type { NextApiRequest, NextApiResponse } from "next" +import sqlite3 from "sqlite3" +import path from "path" + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") return res.status(405).json({ error: "Method not allowed" }) + + const { format = 'json', type = 'sources' } = req.query + + const dbPath = path.join(process.cwd(), "database", "antihoax.db") + const db = new sqlite3.Database(dbPath) + + try { + let query = "" + let filename = "" + + if (type === 'sources') { + query = ` + SELECT s.domain, s.risk_level, s.status, s.created_at, + GROUP_CONCAT(c.name) as categories + FROM sources s + LEFT JOIN source_categories sc ON s.id = sc.source_id + LEFT JOIN categories c ON sc.category_id = c.id + WHERE s.status = 'verified' + GROUP BY s.id + ORDER BY s.risk_level DESC + ` + filename = `sources_export_${Date.now()}.${format}` + } else if (type === 'reports') { + query = ` + SELECT source_url, status, categories, description, created_at + FROM reports + WHERE status != 'spam' + ORDER BY created_at DESC + ` + filename = `reports_export_${Date.now()}.${format}` + } + + const data = await new Promise((resolve, reject) => { + db.all(query, (err, rows) => { + if (err) reject(err) + else resolve(rows) + }) + }) + + if (format === 'csv') { + // Convert to CSV + if (data.length === 0) { + return res.status(200).send('') + } + + const headers = Object.keys(data[0]).join(',') + const csvRows = data.map(row => + Object.values(row).map(value => + typeof value === 'string' && value.includes(',') + ? `"${value.replace(/"/g, '""')}"` + : value + ).join(',') + ) + + const csvContent = [headers, ...csvRows].join('\n') + + res.setHeader('Content-Type', 'text/csv') + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) + res.send(csvContent) + + } else { + // JSON format + res.setHeader('Content-Type', 'application/json') + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`) + res.json({ + exported_at: new Date().toISOString(), + count: data.length, + data + }) + } + + } catch (error) { + console.error('Export error:', error) + res.status(500).json({ error: "Export failed" }) + } finally { + db.close() + } +} \ No newline at end of file