src/features/images.ts
Rama main · réplica de lectura del repositorio original.
/**
* Image proxy route — serves encrypted images from Bunny CDN.
* GET /image/:filename — downloads, decrypts, and serves the image.
* A broken image serves a 1×1 red pixel instead of failing. The broken-image
* marker, a file missing from storage, or a stored file that will not decrypt
* all count as broken. The missing or unreadable cases are reported as errors.
*/
import { decryptBytes } from "#crypto/encryption.ts";
import { notFoundResponse } from "#routes/response.ts";
import type { createRouter } from "#routes/router.ts";
import { getImageProxyUrl } from "#shared/image-proxy-url.ts";
import {
BROKEN_IMAGE_FILENAME,
BROKEN_IMAGE_PNG,
reportBrokenImage,
} from "#shared/images/broken.ts";
import {
downloadRaw,
getMimeTypeFromFilename,
isStorageEnabled,
} from "#shared/storage.ts";
type RouterFn = ReturnType<typeof createRouter>;
/** One year cache (images are immutable, filenames are random UUIDs) */
const IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable";
/** The red-pixel fallback for a broken image. Never cached, so a repaired
* image serves for real on the very next request. */
const brokenImageResponse = (): Response =>
new Response(BROKEN_IMAGE_PNG.buffer as BodyInit, {
headers: { "cache-control": "no-store", "content-type": "image/png" },
});
/** Serve a decrypted image */
const handleImageRequest = async (filename: string): Promise<Response> => {
const mimeType = getMimeTypeFromFilename(filename);
if (!mimeType) return notFoundResponse();
// Only a definitively missing or unreadable file falls back to the red
// pixel. A transient storage failure (bad credentials, an outage) still
// throws and surfaces as the generic 503, so it keeps looking transient.
const encrypted = await downloadRaw(filename);
if (!encrypted) {
reportBrokenImage(`image file ${filename} is missing from storage`);
return brokenImageResponse();
}
try {
const data = await decryptBytes(encrypted);
return new Response(data.buffer as BodyInit, {
headers: {
"cache-control": IMAGE_CACHE_CONTROL,
"content-type": mimeType,
},
});
} catch (error) {
reportBrokenImage(`image file ${filename} could not be decrypted`, error);
return brokenImageResponse();
}
};
/** Route image requests: GET /image/:filename — the filename is the image's
* random uid, so nothing here is enumerable. */
export const routeImage: RouterFn = async (_, path, method) => {
if (method !== "GET") return null;
// The broken-image marker has no stored file behind it — serve its red
// pixel directly, even when storage is not configured.
if (path === getImageProxyUrl(BROKEN_IMAGE_FILENAME)) {
return brokenImageResponse();
}
const match = path.match(/^\/image\/([a-f0-9-]+\.\w+)$/);
if (!match?.[1]) return null;
if (!isStorageEnabled()) return notFoundResponse();
return await handleImageRequest(match[1]);
};