import { db, type RecordModel } from "./db"; // Pictures people upload: repositories' images and organizations' avatars. They're stored in // protected PocketBase file fields and served by the app with their sniffed type. export const MAX_UPLOADED_IMAGE_BYTES = 1024 * 1024; const EXTENSIONS: Record = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" }; /** Recognizes the raster formats we accept by their magic bytes; the browser-reported type isn't trusted. */ function sniffImageType(data: Uint8Array) { const starts = (...bytes: number[]) => bytes.every((b, i) => data[i] === b); if (starts(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)) return "image/png"; if (starts(0xff, 0xd8, 0xff)) return "image/jpeg"; if (starts(0x47, 0x49, 0x46, 0x38)) return "image/gif"; if (starts(0x52, 0x49, 0x46, 0x46) && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp"; return null; } /** The upload as a file named `.` to store, or why it's refused. */ export function uploadedImage(data: Uint8Array, name: string): { file: File; type: string } | { error: string } { if (data.byteLength === 0) return { error: "Choose an image to upload." }; if (data.byteLength > MAX_UPLOADED_IMAGE_BYTES) return { error: "Images can be at most 1 MB." }; const type = sniffImageType(data); if (!type) return { error: "Use a PNG, JPEG, GIF or WebP image." }; return { file: new File([new Uint8Array(data)], `${name}.${EXTENSIONS[type]}`, { type }), type }; } /** A stored image's bytes, or null. */ export async function readStoredImage(record: RecordModel, filename: string, type: string) { const pb = await db(); // The file fields are protected: downloads need a short-lived file token. const res = await fetch(pb.files.getURL(record, filename, { token: await pb.files.getToken() })); if (!res.ok) return null; return { contentType: type, data: new Uint8Array(await res.arrayBuffer()) }; } /** The response for a stored image: cacheable for good (URLs carry `?v=`) and inert. */ export function imageResponse(image: { contentType: string; data: Uint8Array }, visibility: "public" | "private") { return new Response(new Uint8Array(image.data), { headers: { "Content-Type": image.contentType, "X-Content-Type-Options": "nosniff", "Cache-Control": `${visibility}, max-age=31536000, immutable`, "Content-Security-Policy": "default-src 'none'; sandbox", }, }); }