Skip to content

Commit 5f7aaf2

Browse files
mglewisemily-shen
andauthored
Add support for hosted images api operations in the Images Binding (#10153)
Co-authored-by: emily-shen <69125074+emily-shen@users.noreply.github.com>
1 parent f498237 commit 5f7aaf2

File tree

9 files changed

+536
-43
lines changed

9 files changed

+536
-43
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Add Hosted Images CRUD operations to Images binding.
6+
7+
This is an experimental API that only works locally for the moment.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# 🖼️ images
1+
# images
22

3-
This Worker returns information about an image that is POSTed to it.
3+
This Worker returns information about an image that is POSTed to it and can perform Hosted Images CRUD operations.

packages/miniflare/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
getPersistPath,
4949
HELLO_WORLD_PLUGIN_NAME,
5050
HOST_CAPNP_CONNECT,
51+
IMAGES_PLUGIN_NAME,
5152
KV_PLUGIN_NAME,
5253
launchBrowser,
5354
loadExternalPlugins,
@@ -143,6 +144,7 @@ import type {
143144
D1Database,
144145
DurableObjectNamespace,
145146
Fetcher,
147+
ImagesBinding,
146148
KVNamespace,
147149
KVNamespaceListKey,
148150
Queue,
@@ -2735,6 +2737,12 @@ export class Miniflare {
27352737
): Promise<ReplaceWorkersTypes<R2Bucket>> {
27362738
return this.#getProxy(R2_PLUGIN_NAME, bindingName, workerName);
27372739
}
2740+
getImagesBinding(
2741+
bindingName: string,
2742+
workerName?: string
2743+
): Promise<ReplaceWorkersTypes<ImagesBinding>> {
2744+
return this.#getProxy(IMAGES_PLUGIN_NAME, bindingName, workerName);
2745+
}
27382746
getHelloWorldBinding(
27392747
bindingName: string,
27402748
workerName?: string

packages/miniflare/src/plugins/core/proxy/client.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,16 @@ class ProxyStubHandler<T extends object>
342342
},
343343
};
344344
};
345+
const proxy = this.bridge.getProxy(target) as any;
345346
const binding = {
346347
info: (stream: ReadableStream<Uint8Array>) => {
347-
// @ts-expect-error The stream types are mismatched
348-
return (this.bridge.getProxy(target) as ImagesBinding)["info"](stream);
348+
return proxy["info"](stream);
349349
},
350350
input: (stream: ReadableStream<Uint8Array>) => {
351-
return transformer(this.bridge.getProxy(target), stream, []);
351+
return transformer(proxy, stream, []);
352+
},
353+
get hosted(): ImagesBinding["hosted"] {
354+
return proxy["hosted"];
352355
},
353356
};
354357
return binding;
Lines changed: 130 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
1+
import fs from "node:fs/promises";
2+
import SCRIPT_IMAGES_SERVICE from "worker:images/images";
3+
import SCRIPT_KV_NAMESPACE_OBJECT from "worker:kv/namespace";
14
import { z } from "zod";
2-
import { CoreBindings, CoreHeaders } from "../../workers";
5+
import { Service } from "../../runtime";
6+
import { SharedBindings } from "../../workers";
7+
import { KV_NAMESPACE_OBJECT_CLASS_NAME } from "../kv";
38
import {
9+
getMiniflareObjectBindings,
10+
getPersistPath,
411
getUserBindingServiceName,
12+
objectEntryWorker,
13+
PersistenceSchema,
514
Plugin,
615
ProxyNodeBinding,
716
remoteProxyClientWorker,
817
RemoteProxyConnectionString,
18+
SERVICE_LOOPBACK,
919
WORKER_BINDING_SERVICE_LOOPBACK,
1020
} from "../shared";
1121

12-
const IMAGES_LOCAL_FETCHER = /* javascript */ `
13-
export default {
14-
fetch(req, env) {
15-
const request = new Request(req);
16-
request.headers.set("${CoreHeaders.CUSTOM_FETCH_SERVICE}", "${CoreBindings.IMAGES_SERVICE}");
17-
request.headers.set("${CoreHeaders.ORIGINAL_URL}", request.url);
18-
return env.${CoreBindings.SERVICE_LOOPBACK}.fetch(request)
19-
}
20-
}
21-
`;
22-
2322
const ImagesSchema = z.object({
2423
binding: z.string(),
2524
remoteProxyConnectionString: z
@@ -31,10 +30,18 @@ export const ImagesOptionsSchema = z.object({
3130
images: ImagesSchema.optional(),
3231
});
3332

33+
export const ImagesSharedOptionsSchema = z.object({
34+
imagesPersist: PersistenceSchema,
35+
});
36+
3437
export const IMAGES_PLUGIN_NAME = "images";
3538

36-
export const IMAGES_PLUGIN: Plugin<typeof ImagesOptionsSchema> = {
39+
export const IMAGES_PLUGIN: Plugin<
40+
typeof ImagesOptionsSchema,
41+
typeof ImagesSharedOptionsSchema
42+
> = {
3743
options: ImagesOptionsSchema,
44+
sharedOptions: ImagesSharedOptionsSchema,
3845
async getBindings(options) {
3946
if (!options.images) {
4047
return [];
@@ -69,34 +76,120 @@ export const IMAGES_PLUGIN: Plugin<typeof ImagesOptionsSchema> = {
6976
[options.images.binding]: new ProxyNodeBinding(),
7077
};
7178
},
72-
async getServices({ options }) {
79+
async getServices({
80+
options,
81+
sharedOptions,
82+
tmpPath,
83+
defaultPersistRoot,
84+
unsafeStickyBlobs,
85+
}) {
7386
if (!options.images) {
7487
return [];
7588
}
7689

77-
return [
78-
{
79-
name: getUserBindingServiceName(
80-
IMAGES_PLUGIN_NAME,
81-
options.images.binding,
82-
options.images.remoteProxyConnectionString
83-
),
84-
worker: options.images.remoteProxyConnectionString
85-
? remoteProxyClientWorker(
86-
options.images.remoteProxyConnectionString,
87-
options.images.binding
88-
)
89-
: {
90-
modules: [
91-
{
92-
name: "index.worker.js",
93-
esModule: IMAGES_LOCAL_FETCHER,
94-
},
95-
],
96-
compatibilityDate: "2025-04-01",
97-
bindings: [WORKER_BINDING_SERVICE_LOOPBACK],
98-
},
90+
const serviceName = getUserBindingServiceName(
91+
IMAGES_PLUGIN_NAME,
92+
options.images.binding,
93+
options.images.remoteProxyConnectionString
94+
);
95+
96+
if (options.images.remoteProxyConnectionString) {
97+
return [
98+
{
99+
name: serviceName,
100+
worker: remoteProxyClientWorker(
101+
options.images.remoteProxyConnectionString,
102+
options.images.binding
103+
),
104+
},
105+
];
106+
}
107+
108+
const persistPath = getPersistPath(
109+
IMAGES_PLUGIN_NAME,
110+
tmpPath,
111+
defaultPersistRoot,
112+
sharedOptions.imagesPersist
113+
);
114+
115+
await fs.mkdir(persistPath, { recursive: true });
116+
117+
const storageService = {
118+
name: `${IMAGES_PLUGIN_NAME}:storage`,
119+
disk: { path: persistPath, writable: true },
120+
} satisfies Service;
121+
122+
const objectService = {
123+
name: `${IMAGES_PLUGIN_NAME}:ns`,
124+
worker: {
125+
compatibilityDate: "2023-07-24",
126+
compatibilityFlags: ["nodejs_compat", "experimental"],
127+
modules: [
128+
{
129+
name: "namespace.worker.js",
130+
esModule: SCRIPT_KV_NAMESPACE_OBJECT(),
131+
},
132+
],
133+
durableObjectNamespaces: [
134+
{
135+
className: KV_NAMESPACE_OBJECT_CLASS_NAME,
136+
uniqueKey: `miniflare-images-${KV_NAMESPACE_OBJECT_CLASS_NAME}`,
137+
},
138+
],
139+
durableObjectStorage: { localDisk: storageService.name },
140+
bindings: [
141+
{
142+
name: SharedBindings.MAYBE_SERVICE_BLOBS,
143+
service: { name: storageService.name },
144+
},
145+
{
146+
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
147+
service: { name: SERVICE_LOOPBACK },
148+
},
149+
...getMiniflareObjectBindings(unsafeStickyBlobs),
150+
],
99151
},
100-
];
152+
} satisfies Service;
153+
154+
const kvNamespaceService = {
155+
name: `${IMAGES_PLUGIN_NAME}:ns:data`,
156+
worker: objectEntryWorker(
157+
{
158+
serviceName: objectService.name,
159+
className: KV_NAMESPACE_OBJECT_CLASS_NAME,
160+
},
161+
"images-data"
162+
),
163+
} satisfies Service;
164+
165+
const imagesService = {
166+
name: serviceName,
167+
worker: {
168+
compatibilityDate: "2025-04-01",
169+
modules: [
170+
{
171+
name: "images.worker.js",
172+
esModule: SCRIPT_IMAGES_SERVICE(),
173+
},
174+
],
175+
bindings: [
176+
{
177+
name: "IMAGES_STORE",
178+
kvNamespace: { name: kvNamespaceService.name },
179+
},
180+
WORKER_BINDING_SERVICE_LOOPBACK,
181+
],
182+
},
183+
} satisfies Service;
184+
185+
return [storageService, objectService, kvNamespaceService, imagesService];
186+
},
187+
getPersistPath({ imagesPersist }, tmpPath) {
188+
return getPersistPath(
189+
IMAGES_PLUGIN_NAME,
190+
tmpPath,
191+
undefined,
192+
imagesPersist
193+
);
101194
},
102195
};

packages/miniflare/src/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
144144
z.input<typeof WORKFLOWS_PLUGIN.sharedOptions> &
145145
z.input<typeof SECRET_STORE_PLUGIN.sharedOptions> &
146146
z.input<typeof ANALYTICS_ENGINE_PLUGIN.sharedOptions> &
147+
z.input<typeof IMAGES_PLUGIN.sharedOptions> &
147148
z.input<typeof HELLO_WORLD_PLUGIN.sharedOptions>;
148149

149150
export const PLUGIN_ENTRIES = Object.entries(PLUGINS) as [

0 commit comments

Comments
 (0)