feat: Оптимизация маршрутов для получения данных из S3 с использованием вспомогательных функций getS3JsonWithHeaders и getS3TextWithHeaders для улучшения читаемости и обработки ошибок. Упрощение кода и улучшение обработки конфигураций серверов и фильтров.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s

This commit is contained in:
2025-10-03 02:08:23 +07:00
parent b80bd402a2
commit a4bd1ab1d4
10 changed files with 922 additions and 190 deletions
+19 -42
View File
@@ -3,55 +3,32 @@
*/
const { s3, BUCKET_NAME, writeS3JsonObject, invalidateCacheForKey } = require('../services/s3Service');
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
const { GetObjectCommand, HeadObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const validators = require('../lib/validators');
const { getS3JsonWithHeaders } = require('../utils/s3Helpers');
const S3_KEY = 'bgp_data/communities.json';
// GET /api/communities
async function getCommunities(req, res) {
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY })).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: S3_KEY }));
const fileContent = await streamToString(data.Body);
let communities = [];
try {
const parsed = JSON.parse(fileContent);
communities = Array.isArray(parsed) ? parsed : [];
} catch (parseError) {
console.error('Error parsing communities.json:', parseError);
communities = [];
}
// Нормализация
communities = communities
.filter((c) => c && typeof c.value === 'string' && c.value.trim().length > 0)
.map((c) => ({
value: String(c.value).trim(),
name: c.name ? String(c.name) : '',
description: c.description ? String(c.description) : '',
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '',
color: c.color ? String(c.color) : ''
}));
res.json(communities);
} catch (error) {
if (error.code === 'NoSuchKey') {
return res.json([]);
}
console.error('Error reading communities from S3:', error);
return sendError(res, 500, 'Error reading communities from S3', 'E_S3');
}
await getS3JsonWithHeaders(S3_KEY, req, res, {
transform: (communities) => {
// Нормализация
return communities
.filter((c) => c && typeof c.value === 'string' && c.value.trim().length > 0)
.map((c) => ({
value: String(c.value).trim(),
name: c.name ? String(c.name) : '',
description: c.description ? String(c.description) : '',
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '',
color: c.color ? String(c.color) : ''
}));
},
defaultValue: []
});
}
// POST /api/communities
+14 -83
View File
@@ -3,61 +3,25 @@
*/
const { s3, BUCKET_NAME, writeS3TextObject, headMeta } = require('../services/s3Service');
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const { buildNestedGatewayBlocks } = require('../utils/helpers');
const { generateMikrotikConfig } = require('../utils/mikrotikConfig');
const { getS3JsonWithHeaders } = require('../utils/s3Helpers');
// GET /api/filters/generate-config
async function generateConfig(req, res) {
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: 'filters.json' })).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: 'filters.json' }));
const fileContent = await streamToString(data.Body);
let filters = [];
try {
filters = JSON.parse(fileContent);
if (!Array.isArray(filters)) filters = [];
} catch (parseError) {
console.error('Error parsing filters.json:', parseError);
filters = [];
}
if (filters.length === 0) {
return res.json({ config: '// No filters to generate configuration' });
}
// Group filters by gateway
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
await getS3JsonWithHeaders('filters.json', req, res, {
transform: (filters) => {
const config = generateMikrotikConfig(filters);
return { config };
},
defaultValue: [],
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
config += '/routing filter bgp-in-tmp {\n';
config += buildNestedGatewayBlocks(gatewayGroups, 4);
config += '}\n';
res.json({ config });
} catch (error) {
if (error.code === 'NoSuchKey') {
res.json({ config: '// filters.json file not found' });
} else {
console.error(error);
return sendError(res, 500, 'Error generating configuration', 'E_S3');
}
console.error(error);
return sendError(res, 500, 'Error generating configuration', 'E_S3');
}
}
@@ -80,21 +44,7 @@ async function exportConfig(req, res) {
return res.json({ success: false, message: 'Нет фильтров для экспорта' });
}
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
config += '/routing filter bgp-in-tmp {\n';
config += buildNestedGatewayBlocks(gatewayGroups, 4);
config += '}\n';
const config = generateMikrotikConfig(filters);
await writeS3TextObject('mikrotik-frouting-config.txt', config);
res.json({ success: true, message: 'Конфигурация экспортирована в S3' });
} catch (error) {
@@ -106,26 +56,7 @@ async function exportConfig(req, res) {
// POST /api/server-filters/generate-config
async function generateServerFilterConfig(req, res) {
const { filters } = req.body;
if (!Array.isArray(filters) || filters.length === 0) {
return res.json({ config: '// No filters to generate configuration' });
}
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
config += '/routing filter bgp-in-tmp {\n';
config += buildNestedGatewayBlocks(gatewayGroups, 4);
config += '}\n';
const config = generateMikrotikConfig(filters);
res.json({ config });
}
+17 -65
View File
@@ -3,10 +3,11 @@
*/
const { s3, BUCKET_NAME, writeS3TextObject, deleteS3Object, headMeta } = require('../services/s3Service');
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
const { GetObjectCommand, HeadObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const { createJsonDataRoutes } = require('./jsonDataRoutes');
const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers');
// GET /api/server-configs (список серверов)
const serverConfigsListRoutes = createJsonDataRoutes('server-configs.json', (server, i) => {
@@ -19,33 +20,15 @@ const serverConfigsListRoutes = createJsonDataRoutes('server-configs.json', (ser
// GET /api/server-configs/:serverId (конкретная конфигурация)
async function getServerConfig(req, res) {
const { serverId } = req.params;
try {
const head = await s3.send(new HeadObjectCommand({
Bucket: BUCKET_NAME,
Key: `filter-manager/config-${serverId}.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: `filter-manager/config-${serverId}.txt`
}));
const config = await streamToString(data.Body);
res.json({ config });
} catch (error) {
if (error.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json({ config: '// Конфигурация не найдена' });
} else {
console.error(error);
return sendError(res, 500, 'Error reading server config from S3', 'E_S3');
await getS3TextWithHeaders(
`filter-manager/config-${serverId}.txt`,
req,
res,
{
transform: (text) => ({ config: text }),
defaultValue: '// Конфигурация не найдена'
}
}
);
}
// POST /api/server-configs/:serverId (сохранить конфигурацию)
@@ -100,43 +83,12 @@ async function deleteServerComplete(req, res) {
// GET /api/server-filters/:serverId
async function getServerFilters(req, res) {
const { serverId } = req.params;
try {
const head = await s3.send(new HeadObjectCommand({
Bucket: BUCKET_NAME,
Key: `filter-manager/server-filters-${serverId}.json`
})).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: `filter-manager/server-filters-${serverId}.json`
}));
const fileContent = await streamToString(data.Body);
let filters = [];
try {
filters = JSON.parse(fileContent);
if (!Array.isArray(filters)) filters = [];
} catch (parseError) {
console.error('Error parsing server filters:', parseError);
filters = [];
}
res.json(filters);
} catch (error) {
if (error.code === 'NoSuchKey') {
res.json([]);
} else {
console.error(error);
return sendError(res, 500, 'Error reading server filters from S3', 'E_S3');
}
}
await getS3JsonWithHeaders(
`filter-manager/server-filters-${serverId}.json`,
req,
res,
{ defaultValue: [] }
);
}
// POST /api/server-filters/:serverId
+40
View File
@@ -0,0 +1,40 @@
/**
* Утилиты для генерации конфигурации MikroTik
* Устраняют дублирование кода генерации конфигурации
*/
const { buildNestedGatewayBlocks } = require('./helpers');
/**
* Генерация конфигурации MikroTik из массива фильтров
* @param {Array} filters - Массив фильтров с полями {community, gateway}
* @returns {string} Сгенерированная конфигурация
*/
function generateMikrotikConfig(filters) {
if (!Array.isArray(filters) || filters.length === 0) {
return '// No filters to generate configuration';
}
// Группировка фильтров по gateway
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
config += '/routing filter bgp-in-tmp {\n';
config += buildNestedGatewayBlocks(gatewayGroups, 4);
config += '}\n';
return config;
}
module.exports = {
generateMikrotikConfig,
};
+106
View File
@@ -0,0 +1,106 @@
/**
* Универсальные хелперы для работы с S3
* Устраняют дублирование кода в routes
*/
const { s3, BUCKET_NAME } = require('../services/s3Service');
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const { sendError, checkIfNoneMatch } = require('../middleware/errorHandler');
/**
* Универсальный GET для JSON объектов из S3 с заголовками
* @param {string} s3Key - Ключ S3
* @param {object} req - Express request
* @param {object} res - Express response
* @param {object} options - Опции
* @param {function} options.transform - Функция трансформации данных
* @param {*} options.defaultValue - Значение по умолчанию при отсутствии файла
*/
async function getS3JsonWithHeaders(s3Key, req, res, options = {}) {
const { transform = (data) => data, defaultValue = [] } = options;
try {
// Получаем HEAD для установки заголовков
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).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-None-Match
if (checkIfNoneMatch(req, res, etag)) return;
// Получаем объект
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
const fileContent = await streamToString(data.Body);
let parsed;
try {
parsed = JSON.parse(fileContent);
if (!Array.isArray(parsed) && Array.isArray(defaultValue)) {
parsed = defaultValue;
}
} catch (parseError) {
console.error(`Error parsing ${s3Key}:`, parseError);
parsed = defaultValue;
}
// Применяем трансформацию
const result = transform(parsed);
res.json(result);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json(defaultValue);
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
}
}
}
/**
* Универсальный GET для текстовых объектов из S3 с заголовками
* @param {string} s3Key - Ключ S3
* @param {object} req - Express request
* @param {object} res - Express response
* @param {object} options - Опции
* @param {function} options.transform - Функция трансформации текста
* @param {string} options.defaultValue - Значение по умолчанию при отсутствии файла
*/
async function getS3TextWithHeaders(s3Key, req, res, options = {}) {
const { transform = (text) => ({ config: text }), defaultValue = '' } = options;
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).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: s3Key }));
const text = await streamToString(data.Body);
const result = transform(text);
res.json(result);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json(transform(defaultValue));
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
}
}
}
module.exports = {
getS3JsonWithHeaders,
getS3TextWithHeaders,
};