const { createBackup, listBackups, restoreBackup, cleanupOldBackups } = require('../../../maintenance/backup'); const { logError } = require('../../../infrastructure/logger'); const { trackRequest, trackRequestWithTiming } = require('../../../maintenance/metrics'); const { createErrorResponse } = require('../../../infrastructure/error_handler'); const sdk = require('../../../plugins/sdk'); const fs = require('fs').promises; const path = require('path'); async function handleBackupsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; // GET /api/backups - List all backups if (method === 'GET' && urlPath === '/api/backups') { try { const startTime = Date.now(); const backups = await listBackups(); // Add formatted size to each backup (size is already calculated in listBackups for tar.gz) const backupsWithSize = backups.map((backup) => { return { ...backup, size: backup.size || 0, sizeFormatted: sdk.utils.format.formatBytes(backup.size || 0) }; }); const responseTime = Date.now() - startTime; trackRequestWithTiming('/api/backups', true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(backupsWithSize)); } catch (err) { logError('Admin', `Failed to list backups: ${err.message}`); trackRequest('/api/backups', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // POST /api/backups/create - Create manual backup if (method === 'POST' && urlPath === '/api/backups/create') { try { const startTime = Date.now(); // Cleanup before creating backup await cleanupOldBackups(); const backupPath = await createBackup(); const responseTime = Date.now() - startTime; trackRequestWithTiming('/api/backups/create', true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, path: backupPath })); } catch (err) { logError('Admin', `Failed to create backup: ${err.message}`); trackRequest('/api/backups/create', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // POST /api/backups/restore - Restore from backup if (method === 'POST' && urlPath === '/api/backups/restore') { try { let body = ''; for await (const chunk of req) { body += chunk.toString(); } const data = JSON.parse(body); const { backupName } = data; if (!backupName) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'backupName is required' })); trackRequest('/api/backups/restore', false); return true; } const startTime = Date.now(); await restoreBackup(backupName); const responseTime = Date.now() - startTime; trackRequestWithTiming('/api/backups/restore', true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Backup restored successfully' })); } catch (err) { logError('Admin', `Failed to restore backup: ${err.message}`); trackRequest('/api/backups/restore', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // DELETE /api/backups/:id - Delete backup if (method === 'DELETE' && urlPath.startsWith('/api/backups/')) { try { const backupName = urlPath.split('/api/backups/')[1]; if (!backupName) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Backup name is required' })); trackRequest(urlPath, false); return true; } const BACKUP_DIR = process.env.BACKUP_DIR || './backups'; const backupPath = path.join(BACKUP_DIR, backupName); // Verify it's a backup (directory or tar.gz) if (!backupName.startsWith('backup-')) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid backup name' })); trackRequest(urlPath, false); return true; } const startTime = Date.now(); // Check if it's a file (tar.gz) or directory const stats = await fs.stat(backupPath).catch(() => null); if (stats) { if (stats.isFile()) { // Delete tar.gz file await fs.unlink(backupPath); } else { // Delete directory await fs.rm(backupPath, { recursive: true, force: true }); } } const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Backup deleted successfully' })); } catch (err) { logError('Admin', `Failed to delete backup: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // GET /api/backups/:id/metadata - Get backup metadata if (method === 'GET' && urlPath.startsWith('/api/backups/') && urlPath.endsWith('/metadata')) { try { const backupName = urlPath.split('/api/backups/')[1].replace('/metadata', ''); if (!backupName) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Backup name is required' })); trackRequest(urlPath, false); return true; } const BACKUP_DIR = process.env.BACKUP_DIR || './backups'; const backupPath = path.join(BACKUP_DIR, backupName); const startTime = Date.now(); let metadata, restoreSource, extractDir = null; try { // Check if it's a tar.gz file const stats = await fs.stat(backupPath); const isTarGz = backupName.endsWith('.tar.gz') && stats.isFile(); if (isTarGz) { // Extract tar.gz to read metadata const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); extractDir = path.join(BACKUP_DIR, `extract-metadata-${Date.now()}`); await fs.mkdir(extractDir, { recursive: true }); try { await execAsync(`tar -xzf "${backupPath}" -C "${extractDir}"`); const entries = await fs.readdir(extractDir, { withFileTypes: true }); if (entries.length === 1 && entries[0].isDirectory()) { restoreSource = path.join(extractDir, entries[0].name); } else { restoreSource = extractDir; } const metadataPath = path.join(restoreSource, 'metadata.json'); metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8')); } finally { // Clean up extraction if (extractDir) { await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {}); } } } else { // Directory format restoreSource = backupPath; const metadataPath = path.join(backupPath, 'metadata.json'); metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8')); } // Get file sizes (only for directory format, tar.gz files are already compressed) const filesWithSize = await Promise.all( (metadata.files || []).map(async (fileName) => { if (isTarGz) { // For tar.gz, we can't easily get individual file sizes without extracting return { name: fileName, size: 0, sizeFormatted: 'N/A (compressed)', modified: null }; } else { const filePath = path.join(restoreSource, 'cache', fileName); const altPath = path.join(restoreSource, fileName); try { let stats; try { stats = await fs.stat(filePath); } catch { stats = await fs.stat(altPath); } return { name: fileName, size: stats.size, sizeFormatted: sdk.utils.format.formatBytes(stats.size), modified: stats.mtime.toISOString() }; } catch (err) { return { name: fileName, size: 0, sizeFormatted: '0 Bytes', modified: null }; } } }) ); const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ...metadata, files: filesWithSize })); } catch (readErr) { if (extractDir) { await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {}); } throw readErr; } } catch (err) { if (err.code === 'ENOENT') { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Backup not found' })); } else { logError('Admin', `Failed to get backup metadata: ${err.message}`); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } trackRequest(urlPath, false); } return true; } return false; } module.exports = { handleBackupsRoutes };