Files
2026-04-20 01:17:00 +07:00

162 lines
5.4 KiB
JavaScript

/**
* Общие роуты для текстовых данных (domains, asns, ip-ranges)
*/
const { streamPaginatedText, headS3ObjectEtag, writeS3TextObject, headMeta, readS3TextObject } = require('../services/s3Service');
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
const { splitWhitespace } = require('../utils/helpers');
// Кэш для countOnly запросов
const countOnlyCache = { map: new Map(), ttlMs: 10_000 };
function getCountOnlyCache(cacheKey) {
const v = countOnlyCache.map.get(cacheKey);
if (!v) return null;
if (Date.now() > v.at + countOnlyCache.ttlMs) {
countOnlyCache.map.delete(cacheKey);
return null;
}
return v.value;
}
function setCountOnlyCache(cacheKey, value) {
countOnlyCache.map.set(cacheKey, { value, at: Date.now() });
}
function createTextDataGET(s3Key, mapLine, validate, cachePrefix) {
return async (req, res) => {
const { q = '', offset, limit, countOnly, format } = req.query || {};
try {
if (countOnly === 'true') {
const cacheKey = `${cachePrefix}:count:${q}`;
const cached = getCountOnlyCache(cacheKey);
if (cached != null) {
return res.json(format === 'std' ? { items: [], total: cached, meta: {} } : { total: cached });
}
const { total } = await streamPaginatedText({ key: s3Key, q, offset: 0, limit: 0, mapLine: () => ({}) });
setCountOnlyCache(cacheKey, total);
return res.json(format === 'std' ? { items: [], total, meta: {} } : { total });
} else if (Number(limit) > 0) {
const { items, total } = await streamPaginatedText({
key: s3Key,
q,
offset: Number(offset) || 0,
limit: Number(limit) || 0,
mapLine,
});
if (!validate(items)) {
return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
}
return res.json(format === 'std' ? { items, total, meta: {} } : { items, total });
} else {
const head = await headMeta(s3Key);
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;
let fileContent;
try {
const data = await readS3TextObject(s3Key);
fileContent = data.body;
} catch (e) {
if (e?.code === 'NoSuchKey') {
return res.json([]);
}
throw e;
}
const items = fileContent.split('\n').filter((line) => line).map((line) => mapLine(line.trim()));
if (!validate(items)) {
return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
}
if (format === 'std') {
return res.json({ items, total: items.length, meta: {} });
}
res.json(items);
}
} catch (error) {
if (error?.name === 'NoSuchKey' || error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json([]);
} else {
console.error(error);
return sendError(res, 500, 'Error reading storage', 'E_STORAGE', { error: String(error?.message || error) });
}
}
};
}
function createTextDataPOST(s3Key, formatLine, validate, validateItem = null) {
return async (req, res) => {
const { domains, ipRanges, items, etag } = req.body;
const payload = Array.isArray(domains)
? domains
: Array.isArray(ipRanges)
? ipRanges
: Array.isArray(items)
? items
: null;
if (!Array.isArray(payload)) {
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
}
if (!validate(payload || [])) {
return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA');
}
if (validateItem) {
const validationErrors = [];
for (let i = 0; i < payload.length; i++) {
const errors = validateItem(payload[i], i);
if (errors.length > 0) {
validationErrors.push(...errors);
}
}
if (validationErrors.length > 0) {
return sendError(res, 400, 'Validation errors', 'E_VALIDATION', {
errors: validationErrors.slice(0, 10),
});
}
}
const fileContent = (payload || []).map(formatLine).filter(Boolean).join('\n');
try {
let current = null;
const ifMatch = req.headers['if-match'] ? String(req.headers['if-match']).replace(/\"/g, '"') : null;
try {
current = await headS3ObjectEtag(s3Key);
} catch (_) {}
if (current && (ifMatch || etag) && current !== String(ifMatch || etag)) {
return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH');
}
const meta = await writeS3TextObject(s3Key, fileContent);
return sendOk(res, meta);
} catch (error) {
console.error(error);
return sendError(res, 500, 'Error writing storage', 'E_STORAGE');
}
};
}
function createTextDataRoutes(config) {
const { s3Key, mapLine, formatLine, validate, validateItem, cachePrefix } = config;
return {
get: createTextDataGET(s3Key, mapLine, validate, cachePrefix),
post: createTextDataPOST(s3Key, formatLine, validate, validateItem),
};
}
module.exports = {
createTextDataRoutes,
createTextDataGET,
createTextDataPOST,
};