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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m8s
This commit is contained in:
+54
-36
@@ -1,39 +1,4 @@
|
|||||||
// History endpoints (require bucket versioning to be enabled). If versioning disabled, best effort.
|
// History endpoints are registered below after app is initialized
|
||||||
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();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const AWS = require('aws-sdk');
|
const AWS = require('aws-sdk');
|
||||||
@@ -42,6 +7,7 @@ const path = require('path');
|
|||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const Ajv = require('ajv');
|
const Ajv = require('ajv');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3001;
|
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
|
// Serve static files from the React app
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
@@ -1020,6 +1001,43 @@ app.delete('/api/locks/:resource', (req, res) => {
|
|||||||
res.json({ released: true });
|
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
|
// Generate MikroTik configuration from filters
|
||||||
app.get('/api/filters/generate-config', async (req, res) => {
|
app.get('/api/filters/generate-config', async (req, res) => {
|
||||||
const params = {
|
const params = {
|
||||||
|
|||||||
Reference in New Issue
Block a user