feat: Add history endpoints for resource versioning and implement rollback functionality in server, along with UI integration in ASNs, Domains, and IPRanges managers for enhanced data management
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m45s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m45s
This commit is contained in:
+52
-3
@@ -1,3 +1,39 @@
|
||||
// History endpoints (require bucket versioning to be enabled). If versioning disabled, best effort.
|
||||
app.get('/api/history/:resource', async (req, res) => {
|
||||
const { resource } = req.params;
|
||||
const key = resourceToKey(resource);
|
||||
if (!key) return sendError(res, 400, 'Unknown resource', 'E_RESOURCE');
|
||||
try {
|
||||
if (!s3.listObjectVersions) {
|
||||
return sendError(res, 501, 'S3 listObjectVersions not available', 'E_NOT_SUPPORTED');
|
||||
}
|
||||
const out = await s3.listObjectVersions({ Bucket: BUCKET_NAME, Prefix: key, MaxKeys: 20 }).promise();
|
||||
const versions = (out.Versions || [])
|
||||
.filter(v => v.Key === key)
|
||||
.slice(0, 10)
|
||||
.map(v => ({ versionId: v.VersionId, isLatest: v.IsLatest, lastModified: toIso(v.LastModified), size: v.Size, etag: v.ETag }));
|
||||
return res.json({ items: versions });
|
||||
} catch (e) {
|
||||
console.error('history error', e);
|
||||
return sendError(res, 500, 'Error reading history', 'E_S3', { error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/history/:resource/rollback', async (req, res) => {
|
||||
const { resource } = req.params;
|
||||
const { versionId } = req.body || {};
|
||||
const key = resourceToKey(resource);
|
||||
if (!key || !versionId) return sendError(res, 400, 'Bad request', 'E_BAD_REQUEST');
|
||||
try {
|
||||
// Copy specific version over same key to rollback
|
||||
await s3.copyObject({ Bucket: BUCKET_NAME, CopySource: `/${BUCKET_NAME}/${encodeURIComponent(key)}?versionId=${encodeURIComponent(versionId)}`, Key: key }).promise();
|
||||
const meta = await headMeta(key);
|
||||
return sendOk(res, meta);
|
||||
} catch (e) {
|
||||
console.error('rollback error', e);
|
||||
return sendError(res, 500, 'Error rollback', 'E_S3', { error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const AWS = require('aws-sdk');
|
||||
@@ -51,6 +87,16 @@ function sendError(res, status, message, code, details) {
|
||||
return res.status(status).json({ message, code, details });
|
||||
}
|
||||
|
||||
// Map UI resource -> S3 key (for history endpoints)
|
||||
function resourceToKey(resource) {
|
||||
switch (resource) {
|
||||
case 'domains-new': return 'bgp_data/domains_community.txt';
|
||||
case 'ip-ranges': return 'bgp_data/ips.txt';
|
||||
case 'asns': return 'bgp_data/asns.txt';
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Serve static files from the React app
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
@@ -410,7 +456,8 @@ app.post('/api/asns', async (req, res) => {
|
||||
let current = null;
|
||||
try { current = await headS3ObjectEtag('bgp_data/asns.txt'); } catch {}
|
||||
if (current && etag && current !== String(etag)) {
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
|
||||
const meta = await headMeta('bgp_data/asns.txt');
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -498,7 +545,8 @@ app.post('/api/domains-new', async (req, res) => {
|
||||
let current = null;
|
||||
try { current = await headS3ObjectEtag('bgp_data/domains_community.txt'); } catch {}
|
||||
if (current && etag && current !== String(etag)) {
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
|
||||
const meta = await headMeta('bgp_data/domains_community.txt');
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -586,7 +634,8 @@ app.post('/api/ip-ranges', async (req, res) => {
|
||||
let current = null;
|
||||
try { current = await headS3ObjectEtag('bgp_data/ips.txt'); } catch {}
|
||||
if (current && etag && current !== String(etag)) {
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
|
||||
const meta = await headMeta('bgp_data/ips.txt');
|
||||
return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current, meta });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user