import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { HttpError } from './errors.js'; const HOUR_LIMIT = 5; const DAY_LIMIT = 10; const GLOBAL_DAY_LIMIT = 100; export async function createUploadLimiter({ dataDir }) { const filePath = path.join(path.resolve(dataDir), 'index', 'upload-limits.json'); await fs.mkdir(path.dirname(filePath), { recursive: true }); let state = await loadState(filePath); let writeChain = Promise.resolve(); return { async checkAndConsume(ip) { const now = new Date(); const day = now.toISOString().slice(0, 10); const hour = now.toISOString().slice(0, 13); const key = crypto.createHash('sha256').update(ip || 'unknown').digest('hex'); if (state.day !== day) state = { day, global: 0, clients: {} }; const client = state.clients[key] || { day, dayCount: 0, hour, hourCount: 0 }; if (client.day !== day) { client.day = day; client.dayCount = 0; } if (client.hour !== hour) { client.hour = hour; client.hourCount = 0; } if (state.global >= GLOBAL_DAY_LIMIT) { throw new HttpError(429, 'Daily site upload budget reached. Try again tomorrow.'); } if (client.hourCount >= HOUR_LIMIT) { throw new HttpError(429, 'Upload limit reached: 5 images per hour.'); } if (client.dayCount >= DAY_LIMIT) { throw new HttpError(429, 'Upload limit reached: 10 images per day.'); } state.global += 1; client.hourCount += 1; client.dayCount += 1; state.clients[key] = client; writeChain = writeChain.then(() => fs.writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`)); await writeChain; return { remainingHour: Math.max(0, HOUR_LIMIT - client.hourCount), remainingDay: Math.max(0, DAY_LIMIT - client.dayCount), remainingGlobalDay: Math.max(0, GLOBAL_DAY_LIMIT - state.global) }; } }; } async function loadState(filePath) { try { return JSON.parse(await fs.readFile(filePath, 'utf8')); } catch { return { day: new Date().toISOString().slice(0, 10), global: 0, clients: {} }; } }