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<string, string> = { "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 `<name>.<ext>` 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=<upload time>`) 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",
},
});
}