import path from 'node:path'; import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; import crypto from 'node:crypto'; import { Response } from 'express'; import { db } from '../../db/database'; const TREK_PHOTO_DIR = path.join(__dirname, '../../../uploads/photos/trek'); export const CACHE_TTL = 60 * 60 * 1000; // 1 hour const inFlight = new Map>(); export function cacheKey(provider: string, assetId: string, kind: string, ownerId: number): string { return crypto.createHash('sha1').update(`${provider}:${assetId}:${kind}:${ownerId}`).digest('hex'); } function ensureDir(): void { if (!fs.existsSync(TREK_PHOTO_DIR)) { fs.mkdirSync(TREK_PHOTO_DIR, { recursive: true }); } } function cachedFilePath(key: string): string { return path.join(TREK_PHOTO_DIR, `${key}.bin`); } export function getFresh(key: string): { filePath: string; contentType: string } | null { const row = db.prepare( 'SELECT content_type, fetched_at FROM trek_photo_cache_meta WHERE cache_key = ?' ).get(key) as { content_type: string; fetched_at: number } | undefined; if (!row) return null; if (Date.now() - row.fetched_at >= CACHE_TTL) { db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(key); return null; } const fp = cachedFilePath(key); if (!fs.existsSync(fp)) { db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(key); return null; } return { filePath: fp, contentType: row.content_type }; } export async function put(key: string, bytes: Buffer, contentType: string): Promise { ensureDir(); const fp = cachedFilePath(key); const tmp = fp + '.tmp'; await fsPromises.writeFile(tmp, bytes); await fsPromises.rename(tmp, fp); db.prepare( 'INSERT OR REPLACE INTO trek_photo_cache_meta (cache_key, content_type, fetched_at) VALUES (?, ?, ?)' ).run(key, contentType, Date.now()); } export function serveFresh(res: Response, key: string): boolean { const entry = getFresh(key); if (!entry) return false; res.set('Content-Type', entry.contentType); res.set('Cache-Control', 'public, max-age=3600'); res.sendFile(entry.filePath); return true; } export function getInFlight(key: string): Promise | undefined { return inFlight.get(key); } export function setInFlight(key: string, promise: Promise): void { inFlight.set(key, promise); promise.finally(() => inFlight.delete(key)); } export function sweepExpired(): void { const cutoff = Date.now() - CACHE_TTL * 2; const stale = db.prepare( 'SELECT cache_key FROM trek_photo_cache_meta WHERE fetched_at < ?' ).all(cutoff) as { cache_key: string }[]; for (const row of stale) { db.prepare('DELETE FROM trek_photo_cache_meta WHERE cache_key = ?').run(row.cache_key); const fp = cachedFilePath(row.cache_key); if (fs.existsSync(fp)) fs.unlinkSync(fp); } }