refactor: Reintroduce history endpoints for resource versioning and rollback functionality in server, ensuring proper initialization and organization of code
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m8s

This commit is contained in:
2025-08-11 17:41:04 +07:00
parent 6f45015bec
commit 087e0eb491
+54 -36
View File
@@ -1,39 +1,4 @@
// 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) });
}
});
// History endpoints are registered below after app is initialized
require('dotenv').config();
const express = require('express');
const AWS = require('aws-sdk');
@@ -42,6 +7,7 @@ const path = require('path');
const compression = require('compression');
const Ajv = require('ajv');
const net = require('net');
const crypto = require('crypto');
const app = express();
const port = 3001;
@@ -97,6 +63,21 @@ function resourceToKey(resource) {
}
}
// Compute sha256 of string
function sha256OfString(s) {
return crypto.createHash('sha256').update(Buffer.from(String(s), 'utf-8')).digest('hex');
}
function mapAjvErrors(errors) {
if (!Array.isArray(errors)) return [];
return errors.map((e) => ({
message: e.message,
instancePath: e.instancePath,
keyword: e.keyword,
params: e.params,
}));
}
// Serve static files from the React app
app.use(express.static(path.join(__dirname, 'public')));
@@ -1020,6 +1001,43 @@ app.delete('/api/locks/:resource', (req, res) => {
res.json({ released: true });
});
// 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) });
}
});
// Generate MikroTik configuration from filters
app.get('/api/filters/generate-config', async (req, res) => {
const params = {