import sharp from 'sharp'; import { getLieuPhotoById, PHOTO_LARGEURS } from '$lib/server/lieux'; import type { RequestHandler } from './$types'; const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE; contact OKI)'; const TTL_MS = 24 * 60 * 60 * 1000; // 24 h type Largeur = (typeof PHOTO_LARGEURS)[number]; const LARGEUR_DEFAUT: Largeur = 1200; /** Cache mémoire : clé `${id}:${w}`, TTL 24 h. */ const cache = new Map(); /** Déduplication des fetches en vol (même photo demandée en rafale). */ const enVol = new Map>(); /** * Récupère la photo via l'URL Commons stockée dans le corpus (jamais d'URL * fournie par le client — pas de proxy ouvert), puis la retraite avec sharp : * - rotate() : applique l'orientation EXIF avant suppression des métadonnées ; * - métadonnées : sharp ne conserve AUCUNE métadonnée (EXIF/GPS compris) sauf * appel explicite à withMetadata() — le WebP produit est donc propre ; * - conversion WebP + redimensionnement à la largeur demandée. */ async function chargerPhoto(id: string, w: Largeur): Promise { const lieu = getLieuPhotoById(id); if (!lieu) return null; const res = await fetch(lieu.photo.url, { headers: { 'User-Agent': USER_AGENT } }); if (!res.ok) throw new Error(`commons ${res.status}`); const brut = Buffer.from(await res.arrayBuffer()); return sharp(brut) .rotate() .resize({ width: w, withoutEnlargement: true }) .webp({ quality: 82 }) .toBuffer(); } /** * GET /api/photo/[id]?w=400|800|1200 → image/webp. * `id` = uuid du lieu (déjà exposé par /api/round). 404 si lieu inconnu. * L'URL ainsi produite est opaque : elle ne révèle ni le nom du fichier * Commons ni le lieu (audit C4). */ export const GET: RequestHandler = async ({ params, url }) => { const id = params.id; const wParam = url.searchParams.get('w'); let w: Largeur = LARGEUR_DEFAUT; if (wParam !== null) { const n = Number(wParam); if (!(PHOTO_LARGEURS as readonly number[]).includes(n)) { return new Response('Largeur invalide (400, 800 ou 1200)', { status: 400 }); } w = n as Largeur; } const cle = `${id}:${w}`; const hit = cache.get(cle); if (hit && hit.expires > Date.now()) { return reponseWebp(hit.data); } // 404 immédiat si l'id est inconnu (avant tout fetch amont). if (!getLieuPhotoById(id)) { return new Response('Lieu inconnu', { status: 404 }); } try { let promesse = enVol.get(cle); if (!promesse) { promesse = chargerPhoto(id, w).finally(() => enVol.delete(cle)); enVol.set(cle, promesse); } const data = await promesse; if (!data) return new Response('Lieu inconnu', { status: 404 }); cache.set(cle, { expires: Date.now() + TTL_MS, data }); return reponseWebp(data); } catch { return new Response('Photo indisponible', { status: 502 }); } }; function reponseWebp(data: Buffer): Response { return new Response(new Uint8Array(data), { headers: { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=86400, immutable' } }); }