UNPKG

miniflare

Version:

Fun, full-featured, fully-local simulator for Cloudflare Workers

255 lines (252 loc) 10.2 kB
// src/workers/images/images.worker.ts import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; import { getPublicUrl } from "miniflare:shared"; // src/workers/core/constants.ts var CorePaths = { /** Magic proxy used by getPlatformProxy */ PLATFORM_PROXY: "/cdn-cgi/platform-proxy", /** Trigger scheduled event handlers */ SCHEDULED: "/cdn-cgi/handler/scheduled", /** Trigger email event handlers */ EMAIL: "/cdn-cgi/handler/email", /** Handler path prefix for validation */ HANDLER_PREFIX: "/cdn-cgi/handler/", /** Live reload WebSocket endpoint */ LIVE_RELOAD: "/cdn-cgi/mf/reload", /** Local explorer UI and API */ EXPLORER: "/cdn-cgi/explorer", /** Legacy way to trigger scheduled event handlers */ LEGACY_SCHEDULED: "/cdn-cgi/mf/scheduled", /** Stream video serving endpoint */ STREAM_VIDEO: "/cdn-cgi/mf/stream", /** Local image delivery endpoint for serving hosted images */ IMAGE_DELIVERY: "/cdn-cgi/mf/imagedelivery", /** Public R2 bucket object serving endpoint */ R2_PUBLIC: "/cdn-cgi/local/r2/public" }, CoreHeaders = { CUSTOM_FETCH_SERVICE: "MF-Custom-Fetch-Service", CUSTOM_NODE_SERVICE: "MF-Custom-Node-Service", ORIGINAL_URL: "MF-Original-URL", /** * Stores the original hostname when using the `upstream` option. * When requests are proxied to an upstream, the `Host` header is rewritten * to match the upstream. This header preserves the original hostname * so Workers can access it if needed. */ ORIGINAL_HOSTNAME: "MF-Original-Hostname", PROXY_SHARED_SECRET: "MF-Proxy-Shared-Secret", DISABLE_PRETTY_ERROR: "MF-Disable-Pretty-Error", ERROR_STACK: "MF-Experimental-Error-Stack", ROUTE_OVERRIDE: "MF-Route-Override", CF_BLOB: "MF-CF-Blob", /** Used by the Vite plugin to pass through the original `sec-fetch-mode` header */ SEC_FETCH_MODE: "MF-Sec-Fetch-Mode", // API Proxy OP_SECRET: "MF-Op-Secret", OP: "MF-Op", OP_TARGET: "MF-Op-Target", OP_KEY: "MF-Op-Key", OP_SYNC: "MF-Op-Sync", OP_STRINGIFIED_SIZE: "MF-Op-Stringified-Size", OP_RESULT_TYPE: "MF-Op-Result-Type", OP_ORIGINAL_URL: "MF-Op-Original-URL" }, CoreBindings = { SERVICE_LOOPBACK: "MINIFLARE_LOOPBACK", SERVICE_USER_ROUTE_PREFIX: "MINIFLARE_USER_ROUTE_", SERVICE_USER_FALLBACK: "MINIFLARE_USER_FALLBACK", TEXT_CUSTOM_SERVICE: "MINIFLARE_CUSTOM_SERVICE", // Backs the Images binding (`env.IMAGES`) — see imagesLocalFetcher. IMAGES_BINDING_SERVICE: "MINIFLARE_IMAGES_BINDING_SERVICE", // Backs `fetch(url, { cf: { image } })` transforms — see cfImageLocalFetcher. IMAGES_FETCH_SERVICE: "MINIFLARE_IMAGES_FETCH_SERVICE", TEXT_UPSTREAM_URL: "MINIFLARE_UPSTREAM_URL", JSON_CF_BLOB: "CF_BLOB", JSON_ROUTES: "MINIFLARE_ROUTES", JSON_LOG_LEVEL: "MINIFLARE_LOG_LEVEL", DATA_LIVE_RELOAD_SCRIPT: "MINIFLARE_LIVE_RELOAD_SCRIPT", DURABLE_OBJECT_NAMESPACE_PROXY: "MINIFLARE_PROXY", DATA_PROXY_SECRET: "MINIFLARE_PROXY_SECRET", DATA_PROXY_SHARED_SECRET: "MINIFLARE_PROXY_SHARED_SECRET", TRIGGER_HANDLERS: "TRIGGER_HANDLERS", LOG_REQUESTS: "LOG_REQUESTS", STRIP_DISABLE_PRETTY_ERROR: "STRIP_DISABLE_PRETTY_ERROR", SERVICE_LOCAL_EXPLORER: "MINIFLARE_LOCAL_EXPLORER", EXPLORER_DISK: "MINIFLARE_EXPLORER_DISK", JSON_LOCAL_EXPLORER_BINDING_MAP: "LOCAL_EXPLORER_BINDING_MAP", JSON_LOCAL_EXPLORER_WORKER_NAMES: "LOCAL_EXPLORER_WORKER_NAMES", JSON_EXPLORER_WORKER_OPTS: "MINIFLARE_EXPLORER_WORKER_OPTS", SERVICE_CACHE: "MINIFLARE_CACHE", SERVICE_DEV_REGISTRY_PROXY: "MINIFLARE_DEV_REGISTRY_PROXY", JSON_TELEMETRY_CONFIG: "MINIFLARE_TELEMETRY_CONFIG", DEV_REGISTRY_DEBUG_PORT: "DEV_REGISTRY_DEBUG_PORT", SERVICE_STREAM: "MINIFLARE_STREAM", SERVICE_IMAGES_DELIVERY: "MINIFLARE_IMAGES_DELIVERY", SERVICE_R2_PUBLIC: "MINIFLARE_R2_PUBLIC" }; // src/workers/images/images.worker.ts function buildVariantUrl(publicUrl, imageId, variant) { return new URL( `${CorePaths.IMAGE_DELIVERY}/${imageId}/${variant}`, publicUrl ).toString(); } async function withResolvedVariants(metadata, env) { let publicUrl = await getPublicUrl(env[CoreBindings.SERVICE_LOOPBACK]); return { ...metadata, variants: metadata.variants.map( (variant) => buildVariantUrl(publicUrl, metadata.id, variant) ) }; } function base64DecodeArrayBuffer(buffer) { let base64String = new TextDecoder().decode(buffer), binaryString = atob(base64String.trim()), bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i); return bytes.buffer; } async function base64DecodeStream(stream) { let buffer = await new Response(stream).arrayBuffer(); return base64DecodeArrayBuffer(buffer); } var ImageHandleImpl = class extends RpcTarget { #imageId; #env; constructor(imageId, env) { super(), this.#imageId = imageId, this.#env = env; } async details() { let result = await this.#env.IMAGES_STORE.getWithMetadata( this.#imageId, "arrayBuffer" ); return result.metadata === null ? null : withResolvedVariants(result.metadata, this.#env); } async bytes() { let data = await this.#env.IMAGES_STORE.get(this.#imageId, "arrayBuffer"); return data === null ? null : new Blob([data]).stream(); } async update(options) { let existing = await this.#env.IMAGES_STORE.getWithMetadata( this.#imageId, "arrayBuffer" ); if (existing.value === null || existing.metadata === null) throw new Error(`Image not found: ${this.#imageId}`); let updatedMetadata = { ...existing.metadata, requireSignedURLs: options.requireSignedURLs ?? existing.metadata.requireSignedURLs, meta: options.metadata ?? existing.metadata.meta, creator: options.creator ?? existing.metadata.creator }; return await this.#env.IMAGES_STORE.put(this.#imageId, existing.value, { metadata: updatedMetadata }), withResolvedVariants(updatedMetadata, this.#env); } async delete() { return await this.#env.IMAGES_STORE.get( this.#imageId, "arrayBuffer" ) === null ? !1 : (await this.#env.IMAGES_STORE.delete(this.#imageId), !0); } }, ImagesService = class extends WorkerEntrypoint { image(imageId) { return new ImageHandleImpl(imageId, this.env); } async upload(image, options) { let imageData = image; options?.encoding === "base64" && (imageData = image instanceof ArrayBuffer ? base64DecodeArrayBuffer(image) : await base64DecodeStream(image)); let buffer = imageData instanceof ArrayBuffer ? imageData : await new Response(imageData).arrayBuffer(), id = options?.id ?? crypto.randomUUID(), metadata = { id, filename: options?.filename ?? "uploaded.jpg", uploaded: (/* @__PURE__ */ new Date()).toISOString(), requireSignedURLs: options?.requireSignedURLs ?? !1, meta: options?.metadata ?? {}, variants: ["public"], draft: !1, creator: options?.creator }; return await this.env.IMAGES_STORE.put(id, buffer, { metadata }), withResolvedVariants(metadata, this.env); } async list(options) { let limit = options?.limit ?? 50, allImages = [], kvCursor; do { let kvResult = await this.env.IMAGES_STORE.list({ cursor: kvCursor }); for (let key of kvResult.keys) key.metadata && allImages.push(key.metadata); kvCursor = kvResult.list_complete ? void 0 : kvResult.cursor; } while (kvCursor); options?.creator && allImages.splice( 0, allImages.length, ...allImages.filter((i) => i.creator === options.creator) ), allImages.sort((a, b) => { let dateA = a.uploaded ?? "", dateB = b.uploaded ?? "", cmp = dateA.localeCompare(dateB) || a.id.localeCompare(b.id); return options?.sortOrder === "desc" ? -cmp : cmp; }); let startIndex = 0; if (options?.cursor) { let cursorIndex = allImages.findIndex((i) => i.id === options.cursor); cursorIndex >= 0 && (startIndex = cursorIndex + 1); } let page = allImages.slice(startIndex, startIndex + limit), hasMore = startIndex + limit < allImages.length, lastImage = page[page.length - 1], publicUrl = await getPublicUrl( this.env[CoreBindings.SERVICE_LOOPBACK] ); return { images: page.map((metadata) => ({ ...metadata, variants: metadata.variants.map( (variant) => buildVariantUrl(publicUrl, metadata.id, variant) ) })), cursor: hasMore && lastImage ? lastImage.id : void 0, listComplete: !hasMore }; } async #detectContentType(data) { let formData = new FormData(); formData.append("image", new Blob([data])); let infoRequest = new Request("http://placeholder/info", { method: "POST", body: formData }); infoRequest.headers.set( CoreHeaders.CUSTOM_FETCH_SERVICE, CoreBindings.IMAGES_BINDING_SERVICE ); let response = await this.env[CoreBindings.SERVICE_LOOPBACK].fetch(infoRequest); if (response.ok) { let info = await response.json(); if (info.format) return info.format; } return "application/octet-stream"; } // Handle HTTP requests for image delivery and transform operations async fetch(request) { let url = new URL(request.url); if (url.pathname.startsWith(`${CorePaths.IMAGE_DELIVERY}/`)) { let imageId = url.pathname.slice(CorePaths.IMAGE_DELIVERY.length + 1).split("/")[0]; if (!imageId) return new Response("Missing image ID", { status: 400 }); let data = await this.env.IMAGES_STORE.get(imageId, "arrayBuffer"); if (data === null) return new Response("Image not found", { status: 404 }); let contentType = await this.#detectContentType(data); return new Response(data, { headers: { "Content-Type": contentType } }); } let forwardRequest = new Request(request); return forwardRequest.headers.set( CoreHeaders.CUSTOM_FETCH_SERVICE, CoreBindings.IMAGES_BINDING_SERVICE ), forwardRequest.headers.set(CoreHeaders.ORIGINAL_URL, request.url), this.env[CoreBindings.SERVICE_LOOPBACK].fetch(forwardRequest); } }; export { ImagesService as default }; //# sourceMappingURL=images.worker.js.map