feat: Оптимизация маршрутов для получения данных из S3 с использованием вспомогательных функций getS3JsonWithHeaders и getS3TextWithHeaders для улучшения читаемости и обработки ошибок. Упрощение кода и улучшение обработки конфигураций серверов и фильтров.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Утилиты для экспорта данных в CSV/TXT
|
||||
* Устраняют дублирование кода экспорта
|
||||
*/
|
||||
|
||||
/**
|
||||
* Экспорт массива объектов в CSV
|
||||
* @param {Array} items - Массив данных
|
||||
* @param {Array} headers - Массив заголовков ['field1', 'field2']
|
||||
* @param {string} filename - Имя файла
|
||||
*/
|
||||
export function exportToCSV(items, headers, filename) {
|
||||
const csvContent = [
|
||||
headers,
|
||||
...items.map(item => headers.map(h => item[h] || ''))
|
||||
]
|
||||
.map(row => row.map(cell => `"${String(cell || '').replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
downloadFile(csvContent, filename, 'text/csv;charset=utf-8;');
|
||||
}
|
||||
|
||||
/**
|
||||
* Экспорт данных в текстовый файл
|
||||
* @param {string} content - Текстовое содержимое
|
||||
* @param {string} filename - Имя файла
|
||||
*/
|
||||
export function exportToText(content, filename) {
|
||||
downloadFile(content, filename, 'text/plain;charset=utf-8;');
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачивание файла
|
||||
* @param {string} content - Содержимое
|
||||
* @param {string} filename - Имя файла
|
||||
* @param {string} mimeType - MIME тип
|
||||
*/
|
||||
function downloadFile(content, filename, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация имени файла с датой
|
||||
* @param {string} prefix - Префикс имени файла
|
||||
* @param {string} extension - Расширение файла
|
||||
* @returns {string} Имя файла с датой
|
||||
*/
|
||||
export function generateFilename(prefix, extension = 'csv') {
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
return `${prefix}_${date}.${extension}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Утилиты для работы с IP адресами и CIDR
|
||||
* Вынесено из IPRangesManager для переиспользования
|
||||
*/
|
||||
|
||||
/**
|
||||
* Парсинг маски из CIDR
|
||||
*/
|
||||
export function parseMask(cidr) {
|
||||
const parts = String(cidr || '').split('/');
|
||||
const mask = Number(parts[1]);
|
||||
return Number.isFinite(mask) ? mask : NaN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подсчет адресов в CIDR
|
||||
*/
|
||||
export function countAddresses(mask) {
|
||||
if (!Number.isFinite(mask) || mask < 0 || mask > 32) return 0n;
|
||||
return 1n << BigInt(32 - mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсинг октетов IPv4
|
||||
*/
|
||||
export function parseOctets(ip) {
|
||||
const [a, b, c, d] = String(ip || '').split('.').map(x => Number(x));
|
||||
return [a, b, c, d];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка приватного IPv4
|
||||
*/
|
||||
export function isPrivateIPv4(ip) {
|
||||
const [a, b] = parseOctets(ip);
|
||||
if (a === 10) return true; // 10.0.0.0/8
|
||||
if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
||||
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка CGNAT IPv4
|
||||
*/
|
||||
export function isCGNATIPv4(ip) {
|
||||
const [a, b] = parseOctets(ip);
|
||||
return a === 100 && b >= 64 && b <= 127; // 100.64.0.0/10
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение категории IP
|
||||
*/
|
||||
export function getIpCategory(cidr) {
|
||||
const baseIp = String(cidr || '').split('/')[0];
|
||||
if (isCGNATIPv4(baseIp)) return 'cgnat';
|
||||
if (isPrivateIPv4(baseIp)) return 'private';
|
||||
return 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование BigInt с разделителями тысяч
|
||||
*/
|
||||
export function formatBigInt(n) {
|
||||
try {
|
||||
const s = (n ?? 0n).toString();
|
||||
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
} catch {
|
||||
return String(n);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация IP в BigInt
|
||||
*/
|
||||
export function ipToInt(ip) {
|
||||
const [a, b, c, d] = parseOctets(ip);
|
||||
return (BigInt(a) << 24n) | (BigInt(b) << 16n) | (BigInt(c) << 8n) | BigInt(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация BigInt в IP
|
||||
*/
|
||||
export function intToIp(num) {
|
||||
const a = Number((num >> 24n) & 255n);
|
||||
const b = Number((num >> 16n) & 255n);
|
||||
const c = Number((num >> 8n) & 255n);
|
||||
const d = Number(num & 255n);
|
||||
return `${a}.${b}.${c}.${d}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация CIDR в диапазон
|
||||
*/
|
||||
export function cidrToRange(cidr) {
|
||||
const [ip, maskStr] = String(cidr).split('/');
|
||||
const mask = Number(maskStr);
|
||||
const ipNum = ipToInt(ip);
|
||||
const hostBits = 32 - mask;
|
||||
const start = ipNum & (~0n << BigInt(hostBits));
|
||||
const end = start | ((1n << BigInt(hostBits)) - 1n);
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
/**
|
||||
* Подсчет trailing zeros для 32-битного числа
|
||||
*/
|
||||
export function countTrailingZeros32(n) {
|
||||
if (n === 0n) return 32;
|
||||
let zeros = 0;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if ((n & (1n << BigInt(i))) !== 0n) {
|
||||
zeros = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return zeros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floor log2 для BigInt
|
||||
*/
|
||||
export function floorLog2(n) {
|
||||
let p = -1;
|
||||
while (n > 0n) {
|
||||
n >>= 1n;
|
||||
p++;
|
||||
}
|
||||
return p < 0 ? 0 : p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация диапазона в CIDR блоки
|
||||
*/
|
||||
export function rangeToCidrs(start, end) {
|
||||
const res = [];
|
||||
let cur = start;
|
||||
while (cur <= end) {
|
||||
const maxAlignPow = countTrailingZeros32(cur);
|
||||
const remaining = end - cur + 1n;
|
||||
const remainingPow = floorLog2(remaining);
|
||||
const blockPow = Math.min(maxAlignPow, remainingPow);
|
||||
const mask = 32 - blockPow;
|
||||
res.push(`${intToIp(cur)}/${mask}`);
|
||||
cur += 1n << BigInt(blockPow);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Слияние интервалов
|
||||
*/
|
||||
export function mergeIntervals(intervals) {
|
||||
if (intervals.length === 0) return [];
|
||||
intervals.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
||||
const merged = [];
|
||||
let [cs, ce] = intervals[0];
|
||||
for (let i = 1; i < intervals.length; i++) {
|
||||
const [s, e] = intervals[i];
|
||||
if (s <= ce + 1n) {
|
||||
if (e > ce) ce = e;
|
||||
} else {
|
||||
merged.push([cs, ce]);
|
||||
cs = s;
|
||||
ce = e;
|
||||
}
|
||||
}
|
||||
merged.push([cs, ce]);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Суммаризация CIDR блоков
|
||||
*/
|
||||
export function summarizeCidrs(cidrs) {
|
||||
const ranges = cidrs.map(cidrToRange);
|
||||
const merged = mergeIntervals(ranges);
|
||||
const out = [];
|
||||
for (const [s, e] of merged) {
|
||||
out.push(...rangeToCidrs(s, e));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение базового /16 для CIDR
|
||||
*/
|
||||
export function get16BaseCidr(cidr) {
|
||||
const ip = String(cidr).split('/')[0];
|
||||
const ipNum = ipToInt(ip);
|
||||
const start = ipNum & (~0n << 16n);
|
||||
return `${intToIp(start)}/16`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Применение агрессивной агрегации /16
|
||||
*/
|
||||
export function applySupernet16(cidrs) {
|
||||
const buckets = new Map();
|
||||
for (const c of cidrs) {
|
||||
const base16 = get16BaseCidr(c);
|
||||
if (!buckets.has(base16)) buckets.set(base16, []);
|
||||
buckets.get(base16).push(c);
|
||||
}
|
||||
const out = [];
|
||||
for (const [base16, list] of buckets) {
|
||||
if (list.length > 2) out.push(base16);
|
||||
else out.push(...list);
|
||||
}
|
||||
return Array.from(new Set(out));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Утилиты валидации
|
||||
* Централизация всех функций валидации
|
||||
*/
|
||||
|
||||
/**
|
||||
* Валидация FQDN домена
|
||||
*/
|
||||
export function isValidDomain(value) {
|
||||
const v = String(value).trim().toLowerCase();
|
||||
return /^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация ASN
|
||||
*/
|
||||
export function isValidAsn(value) {
|
||||
return /^[0-9]+$/.test(String(value).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация IPv4 адреса
|
||||
*/
|
||||
export function isValidIPv4(ip) {
|
||||
const octets = ip.split('.');
|
||||
if (octets.length !== 4) return false;
|
||||
return octets.every(o => /^\d{1,3}$/.test(o) && Number(o) >= 0 && Number(o) <= 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация CIDR
|
||||
*/
|
||||
export function isValidCidr(value) {
|
||||
const v = String(value).trim();
|
||||
const parts = v.split('/');
|
||||
if (parts.length !== 2) return false;
|
||||
const [ip, mask] = parts;
|
||||
if (!isValidIPv4(ip)) return false;
|
||||
if (!/^\d{1,2}$/.test(mask)) return false;
|
||||
const m = Number(mask);
|
||||
return m >= 0 && m <= 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация BGP Community (поддержка числового и extended формата)
|
||||
*/
|
||||
export function isValidCommunity(value) {
|
||||
return /^(\d+|\d+:\d+)$/.test(String(value).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация IP адреса (поддержка IPv4 и IPv6)
|
||||
*/
|
||||
export function isValidIP(value) {
|
||||
// IPv4
|
||||
if (isValidIPv4(value)) return true;
|
||||
|
||||
// Упрощенная проверка IPv6
|
||||
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::)$/;
|
||||
return ipv6Regex.test(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user