Backend: SQLite storage, EvoBGP integration, filters in SQL

Made-with: Cursor
This commit is contained in:
2026-04-20 01:17:00 +07:00
parent 7f3eaea40a
commit 1c6e6ab24a
27 changed files with 2406 additions and 2542 deletions
+136 -163
View File
@@ -2,10 +2,18 @@
* Разные утилитарные роуты (auto-urls, history, s3 meta, availability, etc.)
*/
const { s3, BUCKET_NAME, writeS3TextObject, writeS3JsonObject, readS3TextObject, headMeta, streamToString } = require('../services/s3Service');
const {
writeS3TextObject,
writeS3JsonObject,
readS3TextObject,
headMeta,
listBlobVersionsForKey,
rollbackBlobVersion,
} = require('../services/s3Service');
const evobgpClient = require('../services/evobgpClient');
const filterRulesStorage = require('../services/filterRulesStorage');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { resourceToKey, toIso } = require('../utils/helpers');
const { GetObjectCommand, PutObjectCommand, ListObjectVersionsCommand, CopyObjectCommand } = require('@aws-sdk/client-s3');
const validators = require('../lib/validators');
const net = require('net');
const http = require('http');
@@ -19,66 +27,97 @@ async function getS3LastModified(req, res) {
{ name: 'domainsNew', key: 'bgp_data/domains_community.txt' },
{ name: 'asns', key: 'bgp_data/asns.txt' },
{ name: 'servers', key: 'servers.json' },
{ name: 'filters', key: 'filters.json' },
{ name: 'filters', key: null },
{ name: 'ipRanges', key: 'bgp_data/ips.txt' },
{ name: 'uiSettings', key: 'bgp_data/rt_ui_settings.json' }
];
const results = await Promise.allSettled(
keys.map(k => headMeta(k.key))
);
const out = {};
results.forEach((r, idx) => {
const name = keys[idx].name;
if (r.status === 'fulfilled' && r.value) {
out[name] = r.value;
} else {
out[name] = null;
for (const k of keys) {
if (k.name === 'filters') {
try {
const head = filterRulesStorage.headMetaForScope('global');
out.filters = {
etag: head.etag,
lastModified: head.lastModified,
contentLength: head.contentLength,
};
} catch {
out.filters = null;
}
continue;
}
});
if (['domainsNew', 'asns', 'ipRanges'].includes(k.name) && evobgpClient.isConfigured()) {
try {
await evobgpClient.fetchRouterListsCatalog();
out[k.name] = { etag: null, lastModified: new Date().toISOString(), contentLength: null };
} catch {
out[k.name] = null;
}
continue;
}
out[k.name] = await headMeta(k.key).catch(() => null);
}
res.json(out);
} catch (error) {
console.error('Error fetching last modified dates from S3:', error);
return sendError(res, 500, 'Error fetching last modified dates from S3', 'E_S3');
console.error('Error fetching last modified dates:', error);
return sendError(res, 500, 'Error fetching last modified dates', 'E_STORAGE');
}
}
const EVOBGP_ONLY_HISTORY_RESOURCES = new Set(['domains-new', 'asns', 'ip-ranges', 'filters']);
// GET /api/history/:resource
async function getHistory(req, res) {
const { resource } = req.params;
const { countOnly, format } = req.query || {};
if (EVOBGP_ONLY_HISTORY_RESOURCES.has(resource)) {
const note =
resource === 'filters'
? 'Фильтры в SQL-таблице app_filter_rules; локальной истории отката через этот API нет'
: 'Версии хранятся в EvoBGP, локальной истории нет';
if (countOnly === 'true') {
return res.json(format === 'std' ? { items: [], total: 0, meta: { noBlobHistory: true } } : { total: 0 });
}
return res.json(
format === 'std'
? { items: [], total: 0, meta: { noBlobHistory: true, note } }
: { items: [], total: 0 },
);
}
const key = resourceToKey(resource);
if (!key) return sendError(res, 400, 'Unknown resource', 'E_RESOURCE');
try {
const out = await s3.send(new ListObjectVersionsCommand({
Bucket: BUCKET_NAME,
Prefix: key,
MaxKeys: 50
const rows = listBlobVersionsForKey(key, 50);
const versionsAll = rows.map((r) => ({
id: r.id,
object_key: r.object_key,
byte_size: r.byte_size,
etag: r.etag,
created_at: r.created_at,
}));
const versionsAll = (out.Versions || []).filter(v => v.Key === key);
if (countOnly === 'true') {
const total = versionsAll.length;
return res.json(format === 'std' ? { items: [], total, meta: {} } : { total });
}
const versions = versionsAll.slice(0, 10).map(v => ({
versionId: v.VersionId,
isLatest: v.IsLatest,
lastModified: toIso(v.LastModified),
size: v.Size,
etag: v.ETag
const versions = versionsAll.slice(0, 10).map((v, idx) => ({
versionId: String(v.id),
isLatest: idx === 0,
lastModified: toIso(v.created_at),
size: v.byte_size,
etag: v.etag,
}));
return res.json(format === 'std' ? { items: versions, total: versionsAll.length, meta: {} } : { items: versions });
} catch (e) {
console.error('history error', e);
return sendError(res, 500, 'Error reading history', 'E_S3', { error: String(e?.message || e) });
return sendError(res, 500, 'Error reading history', 'E_STORAGE', { error: String(e?.message || e) });
}
}
@@ -86,125 +125,76 @@ async function getHistory(req, res) {
async function postRollback(req, res) {
const { resource } = req.params;
const { versionId } = req.body || {};
if (EVOBGP_ONLY_HISTORY_RESOURCES.has(resource)) {
const msg =
resource === 'filters'
? 'Откат версий для фильтров не поддерживается (данные в SQL, не в blob_versions)'
: 'Откат версий для этого справочника не поддерживается (данные в EvoBGP)';
return sendError(res, 400, msg, resource === 'filters' ? 'E_HISTORY_FILTERS_SQL' : 'E_HISTORY_EV');
}
const key = resourceToKey(resource);
if (!key || !versionId) {
return sendError(res, 400, 'Bad request', 'E_BAD_REQUEST');
}
try {
await s3.send(new CopyObjectCommand({
Bucket: BUCKET_NAME,
CopySource: `/${BUCKET_NAME}/${encodeURIComponent(key)}?versionId=${encodeURIComponent(versionId)}`,
Key: key
}));
rollbackBlobVersion(key, versionId);
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) });
return sendError(res, 500, 'Error rollback', 'E_STORAGE', { error: String(e?.message || e) });
}
}
// GET /api/auto-urls
async function getAutoUrls(req, res) {
const { checkIfNoneMatch } = require('../middleware/errorHandler');
try {
const { HeadObjectCommand } = require('@aws-sdk/client-s3');
const head = await s3.send(new HeadObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/auto_url/urls.txt'
})).catch(() => null);
const etag = head?.ETag || null;
if (etag) res.set('ETag', String(etag));
if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString());
if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength));
if (checkIfNoneMatch(req, res, etag)) return;
const data = await s3.send(new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/auto_url/urls.txt'
}));
const fileContent = await streamToString(data.Body);
const urls = fileContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
return { url: parts[0] || '', community: parts[1] || '' };
});
res.json(urls);
} catch (error) {
if (error.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json([]);
} else {
console.error(error);
return sendError(res, 500, 'Error reading auto URLs from S3', 'E_S3');
if (!evobgpClient.isConfigured()) {
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
}
const rows = await evobgpClient.fetchAutoUrlsData();
res.json(rows);
} catch (error) {
console.error(error);
return sendError(res, 500, 'Error reading auto URLs', 'E_EVOBGP');
}
}
// POST /api/auto-urls
async function postAutoUrls(req, res) {
const { urls } = req.body;
const fileContent = urls.map(u => `${u.url} ${u.community}`).join('\n');
if (!Array.isArray(urls)) {
return sendError(res, 400, 'urls must be an array', 'E_BAD_REQUEST');
}
try {
const meta = await writeS3TextObject('bgp_data/auto_url/urls.txt', fileContent);
return sendOk(res, meta);
if (!evobgpClient.isConfigured()) {
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
}
await evobgpClient.saveAutoUrlsData(urls);
return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null });
} catch (error) {
console.error(error);
return sendError(res, 500, 'Error writing auto URLs to S3', 'E_S3');
return sendError(res, 500, 'Error writing auto URLs', 'E_EVOBGP');
}
}
// POST /api/auto-urls/process
async function processAutoUrls(req, res) {
try {
// Загрузка URL списков
let urls = [];
try {
const urlsData = await s3.send(new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/auto_url/urls.txt'
}));
const urlsContent = await streamToString(urlsData.Body);
urls = urlsContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
return { url: parts[0] || '', community: parts[1] || '' };
});
} catch (error) {
if (error.code !== 'NoSuchKey') throw error;
if (!evobgpClient.isConfigured()) {
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
}
const urls = await evobgpClient.fetchAutoUrlsData();
if (urls.length === 0) {
return sendError(res, 400, 'No URLs to process', 'E_BAD_REQUEST');
}
// Загрузка текущих IP и доменов
let currentIps = [];
try {
const ipsData = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt' }));
const ipsContent = await streamToString(ipsData.Body);
currentIps = ipsContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
return { ipRange: parts[0] || '', community: parts[1] || '' };
});
} catch (error) {
if (error.code !== 'NoSuchKey') throw error;
}
let currentDomains = [];
try {
const dData = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt' }));
const dContent = await streamToString(dData.Body);
currentDomains = dContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
return { domain: (parts[0] || '').toLowerCase(), community: parts[1] || '' };
});
} catch (error) {
if (error.code !== 'NoSuchKey') throw error;
}
const currentIps = await evobgpClient.fetchIpRangesData();
const currentDomains = await evobgpClient.fetchDomainsData();
// Обработка URL
const newIps = [];
@@ -256,22 +246,8 @@ async function processAutoUrls(req, res) {
const uniqueNewDomains = newDomains.filter(d => !existingDomains.has(d.domain));
const allDomains = [...currentDomains, ...uniqueNewDomains];
// Сохранение
const updatedIpsContent = allIps.map(i => `${i.ipRange} ${i.community}`).join('\n');
await s3.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/ips.txt',
Body: updatedIpsContent,
ContentType: 'text/plain'
}));
const updatedDomainsContent = allDomains.map(d => `${d.domain} ${d.community}`).join('\n');
await s3.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/domains_community.txt',
Body: updatedDomainsContent,
ContentType: 'text/plain'
}));
await evobgpClient.saveIpRangesData(allIps, currentIps);
await evobgpClient.saveDomainsData(allDomains, currentDomains);
const msg = `Обработано ${urls.length} URL, добавлено ${uniqueNewIps.length} IP и ${uniqueNewDomains.length} доменов`;
return res.json({
@@ -285,7 +261,7 @@ async function processAutoUrls(req, res) {
});
} catch (error) {
console.error('Error processing auto URLs:', error);
return sendError(res, 500, 'Error processing auto URLs', 'E_S3');
return sendError(res, 500, 'Error processing auto URLs', 'E_EVOBGP');
}
}
@@ -357,9 +333,8 @@ function getPingServicesListFromSettings(uiSettings) {
/** Загрузить UI-настройки из S3 (для pingServicesSource и др.) */
async function loadUiSettingsSync() {
try {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/rt_ui_settings.json' }));
const jsonText = await streamToString(data.Body);
const parsed = JSON.parse(jsonText || '{}');
const data = await readS3TextObject('bgp_data/rt_ui_settings.json');
const parsed = JSON.parse(data.body || '{}');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
@@ -598,10 +573,10 @@ async function getServersAvailability(req, res) {
return res.json({ ...availabilityCache.data, cached: true });
}
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'servers.json' }));
let servers = [];
try {
servers = JSON.parse(await streamToString(data.Body));
const data = await readS3TextObject('servers.json');
servers = JSON.parse(data.body);
if (!Array.isArray(servers)) servers = [];
} catch {
servers = [];
@@ -683,12 +658,13 @@ async function updateBgpBackground(req, res) {
// GET /api/ws/url
async function getWsUrl(req, res) {
try {
const settings = await s3.send(new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/rt_ui_settings.json'
})).then(async (d) => {
try { return JSON.parse(await streamToString(d.Body)); } catch { return {}; }
}).catch(() => ({}));
let settings = {};
try {
const d = await readS3TextObject('bgp_data/rt_ui_settings.json');
settings = JSON.parse(d.body || '{}');
} catch (_) {
settings = {};
}
const url = settings?.wsUpdateUrl || process.env.WS_UPDATE_URL || '';
return res.json({ url });
@@ -700,25 +676,22 @@ async function getWsUrl(req, res) {
// GET/POST /api/ui-settings
async function getUiSettings(req, res) {
const { checkIfNoneMatch } = require('../middleware/errorHandler');
try {
const { HeadObjectCommand } = require('@aws-sdk/client-s3');
const head = await s3.send(new HeadObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/rt_ui_settings.json'
})).catch(() => null);
const etag = head?.ETag || null;
const head = await headMeta('bgp_data/rt_ui_settings.json');
const etag = head?.etag || null;
if (etag) res.set('ETag', String(etag));
if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString());
if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength));
if (head?.lastModified) res.set('Last-Modified', new Date(head.lastModified).toUTCString());
if (typeof head?.contentLength === 'number') res.set('Content-Length-Source', String(head.contentLength));
if (checkIfNoneMatch(req, res, etag)) return;
const data = await s3.send(new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: 'bgp_data/rt_ui_settings.json'
}));
const jsonText = await streamToString(data.Body);
let jsonText = '{}';
try {
const data = await readS3TextObject('bgp_data/rt_ui_settings.json');
jsonText = data.body;
} catch (e) {
if (e?.code !== 'NoSuchKey') throw e;
}
let settings = {};
try {
const parsed = JSON.parse(jsonText);
@@ -728,11 +701,11 @@ async function getUiSettings(req, res) {
}
return res.json(settings);
} catch (error) {
if (error.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
if (error?.code === 'NoSuchKey') {
return res.json({});
}
console.error('Error reading ui settings from S3:', error);
return sendError(res, 500, 'Error reading UI settings from S3', 'E_S3');
console.error('Error reading ui settings:', error);
return sendError(res, 500, 'Error reading UI settings', 'E_STORAGE');
}
}
@@ -756,8 +729,8 @@ async function postUiSettings(req, res) {
const meta = await writeS3JsonObject('bgp_data/rt_ui_settings.json', payload);
return sendOk(res, meta);
} catch (error) {
console.error('Error writing UI settings to S3:', error);
return sendError(res, 500, 'Error writing UI settings to S3', 'E_S3', { error: String(error?.message || error) });
console.error('Error writing UI settings:', error);
return sendError(res, 500, 'Error writing UI settings', 'E_STORAGE', { error: String(error?.message || error) });
}
}