reorg
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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 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: 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: formatBytes(stats.size),
|
||||
modified: stats.mtime.toISOString()
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
name: fileName,
|
||||
size: 0,
|
||||
sizeFormatted: '0 B',
|
||||
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;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
module.exports = { handleBackupsRoutes };
|
||||
|
||||
Reference in New Issue
Block a user