Rebasely Beta: data may be purged

examples/storefront Public

TypeScript · 53 lines · 2.5 KiB · at e83464e

// Product images. The merchant uploads one image per product and it is stored in R2 under a key// that ends in a hash of its bytes, so a new image gets a new URL and every URL can be cached// for a year. Browsers load them from the bucket's custom domain (IMAGES_URL), not through this// Worker; see .rebasely/kb/decisions/0008-images-from-the-bucket-domain.md.import { HttpError } from "./http.ts";// Turns an image key into the URL shoppers load it from.export type ImageUrls = (key: string) => string;export function imageUrls(base: string): ImageUrls {  const origin = base.replace(/\/+$/, "");  return (key) => `${origin}/${key}`;}export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;export const IMMUTABLE = "public, max-age=31536000, immutable";const EXTENSIONS: Record<string, string> = {  "image/jpeg": "jpg",  "image/png": "png",  "image/webp": "webp",  "image/avif": "avif",};// Stores the request body as the product's image and points the product at it. The previous// object stays in the bucket: carts and caches may still hold its URL.export async function putProductImage(db: D1Database, bucket: R2Bucket, productId: string, req: Request, now: number): Promise<string> {  const exists = await db.prepare("SELECT 1 AS one FROM products WHERE id = ?").bind(productId).first<number>("one");  if (!exists) throw new HttpError(404, "product_not_found", "No such product.");  const type = (req.headers.get("content-type") ?? "").split(";")[0]!.trim().toLowerCase();  const ext = EXTENSIONS[type];  if (!ext) throw new HttpError(415, "unsupported_image", "Send a JPEG, PNG, WebP or AVIF image.");  if (Number(req.headers.get("content-length") ?? 0) > MAX_IMAGE_BYTES) throw tooLarge();  const bytes = await req.arrayBuffer();  if (bytes.byteLength === 0) throw new HttpError(400, "empty_image", "The image is empty.");  if (bytes.byteLength > MAX_IMAGE_BYTES) throw tooLarge();  const key = `products/${productId}/${await shortHash(bytes)}.${ext}`;  await bucket.put(key, bytes, { httpMetadata: { contentType: type, cacheControl: IMMUTABLE } });  await db.prepare("UPDATE products SET image_key = ?, updated_at = ? WHERE id = ?").bind(key, now, productId).run();  return key;}async function shortHash(bytes: ArrayBuffer): Promise<string> {  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));  return [...digest.slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join("");}function tooLarge(): HttpError {  return new HttpError(413, "image_too_large", "An image is at most 5 MiB.");}