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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m18s
This commit is contained in:
+76
-43
@@ -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) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user