Rebasely Beta: data may be purged

examples/storefront Public

TypeScript · 57 lines · 2.8 KiB · at 582fe0a

// 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.import { HttpError } from "./http.ts";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;}// GET /images/<key>: the object from R2 with its stored headers. A matching If-None-Match gets// a 304 so browsers revalidate without downloading the image again.export async function serveImage(bucket: R2Bucket, key: string, req: Request): Promise<Response> {  if (!key.startsWith("products/")) throw new HttpError(404, "not_found", "No such image.");  const object = await bucket.get(key);  if (!object) throw new HttpError(404, "not_found", "No such image.");  const headers = new Headers();  object.writeHttpMetadata(headers);  headers.set("etag", object.httpEtag);  if (req.headers.get("if-none-match") === object.httpEtag) return new Response(null, { status: 304, headers });  return new Response(object.body, { headers });}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.");}