116 lines
3.5 KiB
JavaScript
116 lines
3.5 KiB
JavaScript
const MONTHS = Object.freeze({
|
|
Jan: 0,
|
|
Feb: 1,
|
|
Mar: 2,
|
|
Apr: 3,
|
|
May: 4,
|
|
Jun: 5,
|
|
Jul: 6,
|
|
Aug: 7,
|
|
Sep: 8,
|
|
Oct: 9,
|
|
Nov: 10,
|
|
Dec: 11
|
|
});
|
|
|
|
function parseDownloadTrafficLogTime(line) {
|
|
if (!line || typeof line !== 'string') return null;
|
|
|
|
const match = line.match(/\[(\d{2})\/([A-Za-z]{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2})\s*([+-]\d{4})?\]/);
|
|
if (!match) return null;
|
|
|
|
const month = MONTHS[match[2]];
|
|
if (month === undefined) return null;
|
|
|
|
const year = Number(match[3]);
|
|
const day = Number(match[1]);
|
|
const hour = Number(match[4]);
|
|
const minute = Number(match[5]);
|
|
const second = Number(match[6]);
|
|
if ([year, day, hour, minute, second].some((value) => !Number.isFinite(value))) return null;
|
|
|
|
let utcMillis = Date.UTC(year, month, day, hour, minute, second);
|
|
const timezone = match[7];
|
|
if (timezone && /^[+-]\d{4}$/.test(timezone)) {
|
|
const sign = timezone[0] === '+' ? 1 : -1;
|
|
const timezoneHours = Number(timezone.slice(1, 3));
|
|
const timezoneMinutes = Number(timezone.slice(3, 5));
|
|
if (timezoneHours > 23 || timezoneMinutes > 59) return null;
|
|
utcMillis -= sign * ((timezoneHours * 60) + timezoneMinutes) * 60 * 1000;
|
|
}
|
|
|
|
const parsed = new Date(utcMillis);
|
|
if (Number.isNaN(parsed.getTime())) return null;
|
|
|
|
// Date.UTC normalizes impossible dates; reject them instead of charging the wrong day.
|
|
const offsetMinutes = timezone
|
|
? (timezone[0] === '+' ? 1 : -1) * ((Number(timezone.slice(1, 3)) * 60) + Number(timezone.slice(3, 5)))
|
|
: 0;
|
|
const localDate = new Date(parsed.getTime() + (offsetMinutes * 60 * 1000));
|
|
if (
|
|
localDate.getUTCFullYear() !== year
|
|
|| localDate.getUTCMonth() !== month
|
|
|| localDate.getUTCDate() !== day
|
|
|| localDate.getUTCHours() !== hour
|
|
|| localDate.getUTCMinutes() !== minute
|
|
|| localDate.getUTCSeconds() !== second
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function parseDownloadTrafficLine(line, fallbackDate = new Date()) {
|
|
if (!line || typeof line !== 'string') return null;
|
|
|
|
const trimmed = line.trim();
|
|
if (!trimmed || !/\bGET\b/i.test(trimmed)) return null;
|
|
|
|
const statusMatch = trimmed.match(/"\s*(\d{3})\s+(\d+|-)\b/);
|
|
if (!statusMatch) return null;
|
|
|
|
const statusCode = Number(statusMatch[1]);
|
|
const bytesSent = statusMatch[2] === '-' ? 0 : Number(statusMatch[2]);
|
|
if (![200, 206].includes(statusCode) || !Number.isFinite(bytesSent) || bytesSent <= 0) return null;
|
|
|
|
let objectKey = null;
|
|
const requestMatch = trimmed.match(/"(?:GET|HEAD)\s+([^" ]+)\s+HTTP\//i);
|
|
if (requestMatch?.[1]) {
|
|
let requestPath = requestMatch[1];
|
|
const queryIndex = requestPath.indexOf('?');
|
|
if (queryIndex >= 0) requestPath = requestPath.slice(0, queryIndex);
|
|
requestPath = requestPath.replace(/^https?:\/\/[^/]+/i, '').replace(/^\/+/, '');
|
|
try {
|
|
requestPath = decodeURIComponent(requestPath);
|
|
} catch {
|
|
// Keep the raw path when the log contains malformed percent encoding.
|
|
}
|
|
objectKey = requestPath || null;
|
|
}
|
|
|
|
if (!objectKey) {
|
|
const keyMatch = trimmed.match(/\buser_(\d+)\/[^\s"]+/);
|
|
objectKey = keyMatch?.[0] || null;
|
|
}
|
|
if (!objectKey) return null;
|
|
|
|
const userMatch = objectKey.match(/(?:^|\/)user_(\d+)\//);
|
|
if (!userMatch) return null;
|
|
|
|
const userId = Number(userMatch[1]);
|
|
if (!Number.isSafeInteger(userId) || userId <= 0) return null;
|
|
|
|
return {
|
|
userId,
|
|
bytes: Math.floor(bytesSent),
|
|
objectKey,
|
|
eventAt: parseDownloadTrafficLogTime(trimmed) || fallbackDate
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
parseDownloadTrafficLine,
|
|
parseDownloadTrafficLogTime
|
|
};
|