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();
});
// 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
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
} else {
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
app.get('/api/asns', async (req, res) => {
const { q = '', offset, limit } = req.query || {};
const { q = '', offset, limit, countOnly } = req.query || {};
const params = {
Bucket: BUCKET_NAME,
Key: 'bgp_data/asns.txt',
};
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({
key: 'bgp_data/asns.txt',
q,
@@ -335,7 +371,7 @@ app.get('/api/asns', async (req, res) => {
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 });
} else {
const data = await s3.getObject(params).promise();
@@ -346,7 +382,7 @@ app.get('/api/asns', async (req, res) => {
const type = parts[1] || '';
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.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
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 {
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;
try { current = await headS3ObjectEtag('bgp_data/asns.txt'); } catch {}
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 {}
@@ -387,16 +423,11 @@ app.post('/api/asns', async (req, res) => {
try {
const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag));
try {
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 });
const meta = await headMeta('bgp_data/asns.txt');
return sendOk(res, meta);
} catch (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
app.get('/api/domains-new', async (req, res) => {
const { q = '', offset, limit } = req.query || {};
const { q = '', offset, limit, countOnly } = req.query || {};
const params = {
Bucket: BUCKET_NAME,
Key: 'bgp_data/domains_community.txt',
};
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({
key: 'bgp_data/domains_community.txt',
q,
@@ -422,7 +459,7 @@ app.get('/api/domains-new', async (req, res) => {
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 });
} else {
const data = await s3.getObject(params).promise();
@@ -433,7 +470,7 @@ app.get('/api/domains-new', async (req, res) => {
const community = parts[1] || '';
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.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
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 {
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;
try { current = await headS3ObjectEtag('bgp_data/domains_community.txt'); } catch {}
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 {}
@@ -474,16 +511,11 @@ app.post('/api/domains-new', async (req, res) => {
try {
const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag));
try {
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 });
const meta = await headMeta('bgp_data/domains_community.txt');
return sendOk(res, meta);
} catch (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
app.get('/api/ip-ranges', async (req, res) => {
const { q = '', offset, limit } = req.query || {};
const { q = '', offset, limit, countOnly } = req.query || {};
const params = {
Bucket: BUCKET_NAME,
Key: 'bgp_data/ips.txt',
};
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({
key: 'bgp_data/ips.txt',
q,
@@ -509,7 +547,7 @@ app.get('/api/ip-ranges', async (req, res) => {
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 });
} else {
const data = await s3.getObject(params).promise();
@@ -520,7 +558,7 @@ app.get('/api/ip-ranges', async (req, res) => {
const community = parts[1] || '';
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.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString());
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 {
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;
try { current = await headS3ObjectEtag('bgp_data/ips.txt'); } catch {}
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 {}
@@ -561,16 +599,11 @@ app.post('/api/ip-ranges', async (req, res) => {
try {
const put = await s3.putObject(params).promise();
if (put.ETag) res.set('ETag', String(put.ETag));
try {
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 });
const meta = await headMeta('bgp_data/ips.txt');
return sendOk(res, meta);
} catch (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) });
}
});