Files
p2ns/includes/admin/admin-backend/routes/backups.js
T
Raven Scott ad4784d05d feat: consolidate utility functions into P2NS Plugin SDK
- Centralized common formatting, DOM, and status utilities in `includes/plugins/sdk.js`
- Created `sdk.utils.format`, `sdk.utils.dom`, and `sdk.utils.status` namespaces
- Refactored `domain.consensus` and `peer.visualize` plugins to use the global SDK
- Updated `plugin-handler` to serve SDK utilities globally via `/sdk-utils.js`
- Enhanced SDK validation by delegating to the core validation infrastructure
- Fixed bugs in metric rendering and missing frontend utility functions
2025-12-17 22:21:58 -05:00

270 lines
10 KiB
JavaScript

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 };