feat: Introduce helper functions for S3 metadata handling and improve error responses across ASNs, Domains, and IP Ranges APIs for better consistency and clarity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m18s

This commit is contained in:
2025-08-11 16:54:19 +07:00
parent f1b84a761e
commit 7321e21871
+76 -43
View File
@@ -22,6 +22,35 @@ app.use((req, res, next) => {
next(); next();
}); });
// Helpers: meta and responses
function toIso(x) {
try { return new Date(x).toISOString(); } catch { return null; }
}
async function headMeta(key) {
try {
const h = await s3.headObject({ Bucket: BUCKET_NAME, Key: key }).promise();
return {
etag: h.ETag || null,
lastModified: h.LastModified ? toIso(h.LastModified) : null,
contentLength: typeof h.ContentLength === 'number' ? h.ContentLength : null,
};
} catch (e) {
return { etag: null, lastModified: null, contentLength: null };
}
}
function sendOk(res, meta) {
if (meta?.etag) res.set('ETag', String(meta.etag));
if (meta?.lastModified) res.set('Last-Modified', new Date(meta.lastModified).toUTCString());
if (typeof meta?.contentLength === 'number') res.set('Content-Length-Source', String(meta.contentLength));
return res.json({ ok: true, etag: meta?.etag || null, lastModified: meta?.lastModified || null, contentLength: meta?.contentLength ?? null });
}
function sendError(res, status, message, code, details) {
return res.status(status).json({ message, code, details });
}
// 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')));
@@ -274,7 +303,7 @@ app.get('/api/domains', async (req, res) => {
res.json([]); // Return empty array if file does not exist res.json([]); // Return empty array if file does not exist
} else { } else {
console.error(error); console.error(error);
res.status(500).send('Error reading from S3'); return sendError(res, 500, 'Error reading from S3', 'E_S3', { error: String(error?.message || error) });
} }
} }
}); });
@@ -317,14 +346,21 @@ app.post('/api/domains', async (req, res) => {
// Get ASNs from S3 // Get ASNs from S3
app.get('/api/asns', async (req, res) => { app.get('/api/asns', async (req, res) => {
const { q = '', offset, limit } = req.query || {}; const { q = '', offset, limit, countOnly } = req.query || {};
const params = { const params = {
Bucket: BUCKET_NAME, Bucket: BUCKET_NAME,
Key: 'bgp_data/asns.txt', Key: 'bgp_data/asns.txt',
}; };
try { try {
if (limit !== undefined) { if (countOnly === 'true') {
// только количество
const { total } = await streamPaginatedText({
key: 'bgp_data/asns.txt', q, offset: 0, limit: 0,
mapLine: (line) => ({})
});
return res.json({ total });
} else if (limit !== undefined) {
const { items, total } = await streamPaginatedText({ const { items, total } = await streamPaginatedText({
key: 'bgp_data/asns.txt', key: 'bgp_data/asns.txt',
q, q,
@@ -335,7 +371,7 @@ app.get('/api/asns', async (req, res) => {
return { domain: parts[0] || '', type: parts[1] || '' }; return { domain: parts[0] || '', type: parts[1] || '' };
} }
}); });
if (!validateAsns(items)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateAsns(items)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
return res.json({ items, total }); return res.json({ items, total });
} else { } else {
const data = await s3.getObject(params).promise(); const data = await s3.getObject(params).promise();
@@ -346,7 +382,7 @@ app.get('/api/asns', async (req, res) => {
const type = parts[1] || ''; const type = parts[1] || '';
return { domain, type }; return { domain, type };
}); });
if (!validateAsns(asns)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateAsns(asns)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
if (data.ETag) res.set('ETag', String(data.ETag)); if (data.ETag) res.set('ETag', String(data.ETag));
if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength));
@@ -369,12 +405,12 @@ app.post('/api/asns', async (req, res) => {
try { try {
if (!validateAsns(asns || [])) { if (!validateAsns(asns || [])) {
return res.status(400).json({ message: 'Invalid payload format for asns' }); return sendError(res, 400, 'Invalid payload format for asns', 'E_SCHEMA');
} }
let current = null; let current = null;
try { current = await headS3ObjectEtag('bgp_data/asns.txt'); } catch {} try { current = await headS3ObjectEtag('bgp_data/asns.txt'); } catch {}
if (current && etag && current !== String(etag)) { if (current && etag && current !== String(etag)) {
return res.status(412).json({ message: 'Precondition Failed: ETag mismatch', currentEtag: current }); return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
} }
} catch {} } catch {}
@@ -387,16 +423,11 @@ app.post('/api/asns', async (req, res) => {
try { try {
const put = await s3.putObject(params).promise(); const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag)); const meta = await headMeta('bgp_data/asns.txt');
try { return sendOk(res, meta);
const head = await s3.headObject({ Bucket: BUCKET_NAME, Key: 'bgp_data/asns.txt' }).promise();
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));
} catch {}
res.json({ ok: true });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).send('Error writing to S3'); return sendError(res, 500, 'Error writing to S3', 'E_S3', { error: String(error?.message || error) });
} }
}); });
@@ -404,14 +435,20 @@ app.post('/api/asns', async (req, res) => {
// Get domains-new from S3 // Get domains-new from S3
app.get('/api/domains-new', async (req, res) => { app.get('/api/domains-new', async (req, res) => {
const { q = '', offset, limit } = req.query || {}; const { q = '', offset, limit, countOnly } = req.query || {};
const params = { const params = {
Bucket: BUCKET_NAME, Bucket: BUCKET_NAME,
Key: 'bgp_data/domains_community.txt', Key: 'bgp_data/domains_community.txt',
}; };
try { try {
if (limit !== undefined) { if (countOnly === 'true') {
const { total } = await streamPaginatedText({
key: 'bgp_data/domains_community.txt', q, offset: 0, limit: 0,
mapLine: (line) => ({})
});
return res.json({ total });
} else if (limit !== undefined) {
const { items, total } = await streamPaginatedText({ const { items, total } = await streamPaginatedText({
key: 'bgp_data/domains_community.txt', key: 'bgp_data/domains_community.txt',
q, q,
@@ -422,7 +459,7 @@ app.get('/api/domains-new', async (req, res) => {
return { domain: parts[0] || '', community: parts[1] || '' }; return { domain: parts[0] || '', community: parts[1] || '' };
} }
}); });
if (!validateDomainsNew(items)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateDomainsNew(items)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
return res.json({ items, total }); return res.json({ items, total });
} else { } else {
const data = await s3.getObject(params).promise(); const data = await s3.getObject(params).promise();
@@ -433,7 +470,7 @@ app.get('/api/domains-new', async (req, res) => {
const community = parts[1] || ''; const community = parts[1] || '';
return { domain, community }; return { domain, community };
}); });
if (!validateDomainsNew(domains)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateDomainsNew(domains)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
if (data.ETag) res.set('ETag', String(data.ETag)); if (data.ETag) res.set('ETag', String(data.ETag));
if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength));
@@ -456,12 +493,12 @@ app.post('/api/domains-new', async (req, res) => {
try { try {
if (!validateDomainsNew(domains || [])) { if (!validateDomainsNew(domains || [])) {
return res.status(400).json({ message: 'Invalid payload format for domains-new' }); return sendError(res, 400, 'Invalid payload format for domains-new', 'E_SCHEMA');
} }
let current = null; let current = null;
try { current = await headS3ObjectEtag('bgp_data/domains_community.txt'); } catch {} try { current = await headS3ObjectEtag('bgp_data/domains_community.txt'); } catch {}
if (current && etag && current !== String(etag)) { if (current && etag && current !== String(etag)) {
return res.status(412).json({ message: 'Precondition Failed: ETag mismatch', currentEtag: current }); return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
} }
} catch {} } catch {}
@@ -474,16 +511,11 @@ app.post('/api/domains-new', async (req, res) => {
try { try {
const put = await s3.putObject(params).promise(); const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag)); const meta = await headMeta('bgp_data/domains_community.txt');
try { return sendOk(res, meta);
const head = await s3.headObject({ Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt' }).promise();
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));
} catch {}
res.json({ ok: true });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).send('Error writing to S3'); return sendError(res, 500, 'Error writing to S3', 'E_S3', { error: String(error?.message || error) });
} }
}); });
@@ -491,14 +523,20 @@ app.post('/api/domains-new', async (req, res) => {
// Get IP ranges from S3 // Get IP ranges from S3
app.get('/api/ip-ranges', async (req, res) => { app.get('/api/ip-ranges', async (req, res) => {
const { q = '', offset, limit } = req.query || {}; const { q = '', offset, limit, countOnly } = req.query || {};
const params = { const params = {
Bucket: BUCKET_NAME, Bucket: BUCKET_NAME,
Key: 'bgp_data/ips.txt', Key: 'bgp_data/ips.txt',
}; };
try { try {
if (limit !== undefined) { if (countOnly === 'true') {
const { total } = await streamPaginatedText({
key: 'bgp_data/ips.txt', q, offset: 0, limit: 0,
mapLine: (line) => ({})
});
return res.json({ total });
} else if (limit !== undefined) {
const { items, total } = await streamPaginatedText({ const { items, total } = await streamPaginatedText({
key: 'bgp_data/ips.txt', key: 'bgp_data/ips.txt',
q, q,
@@ -509,7 +547,7 @@ app.get('/api/ip-ranges', async (req, res) => {
return { ipRange: parts[0] || '', community: parts[1] || '' }; return { ipRange: parts[0] || '', community: parts[1] || '' };
} }
}); });
if (!validateIpRanges(items)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateIpRanges(items)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
return res.json({ items, total }); return res.json({ items, total });
} else { } else {
const data = await s3.getObject(params).promise(); const data = await s3.getObject(params).promise();
@@ -520,7 +558,7 @@ app.get('/api/ip-ranges', async (req, res) => {
const community = parts[1] || ''; const community = parts[1] || '';
return { ipRange, community }; return { ipRange, community };
}); });
if (!validateIpRanges(ipRanges)) return res.status(500).json({ message: 'Invalid data format' }); if (!validateIpRanges(ipRanges)) return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
if (data.ETag) res.set('ETag', String(data.ETag)); if (data.ETag) res.set('ETag', String(data.ETag));
if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength));
@@ -543,12 +581,12 @@ app.post('/api/ip-ranges', async (req, res) => {
try { try {
if (!validateIpRanges(ipRanges || [])) { if (!validateIpRanges(ipRanges || [])) {
return res.status(400).json({ message: 'Invalid payload format for ip-ranges' }); return sendError(res, 400, 'Invalid payload format for ip-ranges', 'E_SCHEMA');
} }
let current = null; let current = null;
try { current = await headS3ObjectEtag('bgp_data/ips.txt'); } catch {} try { current = await headS3ObjectEtag('bgp_data/ips.txt'); } catch {}
if (current && etag && current !== String(etag)) { if (current && etag && current !== String(etag)) {
return res.status(412).json({ message: 'Precondition Failed: ETag mismatch', currentEtag: current }); return sendError(res, 412, 'Precondition Failed: ETag mismatch', 'E_ETAG_MISMATCH', { currentEtag: current });
} }
} catch {} } catch {}
@@ -561,16 +599,11 @@ app.post('/api/ip-ranges', async (req, res) => {
try { try {
const put = await s3.putObject(params).promise(); const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag)); const meta = await headMeta('bgp_data/ips.txt');
try { return sendOk(res, meta);
const head = await s3.headObject({ Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt' }).promise();
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));
} catch {}
res.json({ ok: true });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).send('Error writing to S3'); return sendError(res, 500, 'Error writing to S3', 'E_S3', { error: String(error?.message || error) });
} }
}); });