mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 21:06:04 +00:00
refactor and move cloudflare image transformation plugin
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
|
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||||
|
import { ImageOptimizationPlugin } from "./plugins/image-optimization.plugin";
|
||||||
|
|
||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
export { makeApp, getFresh } from "./modes/fresh";
|
export { makeApp, getFresh } from "./modes/fresh";
|
||||||
@@ -13,6 +14,9 @@ export {
|
|||||||
type BindingMap,
|
type BindingMap,
|
||||||
} from "./bindings";
|
} from "./bindings";
|
||||||
export { constants } from "./config";
|
export { constants } from "./config";
|
||||||
|
export const plugins = {
|
||||||
|
imageOptimization: ImageOptimizationPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
export function d1(config: D1ConnectionConfig) {
|
export function d1(config: D1ConnectionConfig) {
|
||||||
return new D1Connection(config);
|
return new D1Connection(config);
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { App, AppPlugin } from "bknd";
|
||||||
|
|
||||||
|
export type ImageOptimizationPluginOptions = {
|
||||||
|
accessUrl?: string;
|
||||||
|
resolvePath?: string;
|
||||||
|
autoFormat?: boolean;
|
||||||
|
devBypass?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ImageOptimizationPlugin({
|
||||||
|
accessUrl = "/_plugin/image/optimize",
|
||||||
|
resolvePath = "/api/media/file",
|
||||||
|
autoFormat = true,
|
||||||
|
devBypass,
|
||||||
|
}: ImageOptimizationPluginOptions = {}): AppPlugin {
|
||||||
|
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"];
|
||||||
|
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
|
||||||
|
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (app: App) => ({
|
||||||
|
name: "cf-image-optimization",
|
||||||
|
onBuilt: () => {
|
||||||
|
app.server.get(`${accessUrl}/:path{.+$}`, async (c) => {
|
||||||
|
const request = c.req.raw;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (devBypass) {
|
||||||
|
return c.redirect(devBypass + url.pathname + url.search, 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = app.module.media?.storage;
|
||||||
|
if (!storage) {
|
||||||
|
throw new Error("No media storage configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = c.req.param("path");
|
||||||
|
if (!path) {
|
||||||
|
throw new Error("No url provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageURL = `${url.origin}${resolvePath}/${path}`;
|
||||||
|
const metadata = await storage.objectMetadata(path);
|
||||||
|
|
||||||
|
// Cloudflare-specific options are in the cf object.
|
||||||
|
const params = Object.fromEntries(url.searchParams.entries());
|
||||||
|
const options: RequestInitCfPropertiesImage = {};
|
||||||
|
|
||||||
|
// Copy parameters from query string to request options.
|
||||||
|
// You can implement various different parameters here.
|
||||||
|
if ("fit" in params) options.fit = params.fit as any;
|
||||||
|
if ("width" in params) options.width = Number.parseInt(params.width);
|
||||||
|
if ("height" in params) options.height = Number.parseInt(params.height);
|
||||||
|
if ("quality" in params) options.quality = Number.parseInt(params.quality);
|
||||||
|
|
||||||
|
// Your Worker is responsible for automatic format negotiation. Check the Accept header.
|
||||||
|
if (autoFormat) {
|
||||||
|
const accept = request.headers.get("Accept")!;
|
||||||
|
if (/image\/avif/.test(accept)) {
|
||||||
|
options.format = "avif";
|
||||||
|
} else if (/image\/webp/.test(accept)) {
|
||||||
|
options.format = "webp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a request that passes through request headers
|
||||||
|
const imageRequest = new Request(imageURL, {
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Returning fetch() with resizing options will pass through response with the resized image.
|
||||||
|
const res = await fetch(imageRequest, { cf: { image: options } });
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "public, max-age=600",
|
||||||
|
"Content-Type": metadata.type,
|
||||||
|
"Content-Length": metadata.size.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import type { App } from "../../App";
|
|
||||||
|
|
||||||
export type ImageOptimizationPluginOptions = {
|
|
||||||
accessUrl?: string;
|
|
||||||
resolvePath?: string;
|
|
||||||
autoFormat?: boolean;
|
|
||||||
devBypass?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ImageOptimizationPlugin({
|
|
||||||
accessUrl = "/_plugin/image/optimize",
|
|
||||||
resolvePath = "/api/media/file",
|
|
||||||
autoFormat = true,
|
|
||||||
devBypass,
|
|
||||||
}: ImageOptimizationPluginOptions = {}) {
|
|
||||||
const disallowedAccessUrls = ["/api", "/admin", "/_optimize"];
|
|
||||||
if (disallowedAccessUrls.includes(accessUrl) || accessUrl.length < 2) {
|
|
||||||
throw new Error(`Disallowed accessUrl: ${accessUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (app: App) => {
|
|
||||||
app.module.server.client.get(`${accessUrl}/:path{.+$}`, async (c) => {
|
|
||||||
const request = c.req.raw;
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
if (devBypass) {
|
|
||||||
return c.redirect(devBypass + url.pathname + url.search, 302);
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage = app.module.media?.storage;
|
|
||||||
if (!storage) {
|
|
||||||
throw new Error("No media storage configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = c.req.param("path");
|
|
||||||
if (!path) {
|
|
||||||
throw new Error("No url provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageURL = `${url.origin}${resolvePath}/${path}`;
|
|
||||||
const metadata = await storage.objectMetadata(path);
|
|
||||||
|
|
||||||
// Cloudflare-specific options are in the cf object.
|
|
||||||
const params = Object.fromEntries(url.searchParams.entries());
|
|
||||||
const options: RequestInitCfPropertiesImage = {};
|
|
||||||
|
|
||||||
// Copy parameters from query string to request options.
|
|
||||||
// You can implement various different parameters here.
|
|
||||||
if ("fit" in params) options.fit = params.fit as any;
|
|
||||||
if ("width" in params) options.width = Number.parseInt(params.width);
|
|
||||||
if ("height" in params) options.height = Number.parseInt(params.height);
|
|
||||||
if ("quality" in params) options.quality = Number.parseInt(params.quality);
|
|
||||||
|
|
||||||
// Your Worker is responsible for automatic format negotiation. Check the Accept header.
|
|
||||||
if (autoFormat) {
|
|
||||||
const accept = request.headers.get("Accept")!;
|
|
||||||
if (/image\/avif/.test(accept)) {
|
|
||||||
options.format = "avif";
|
|
||||||
} else if (/image\/webp/.test(accept)) {
|
|
||||||
options.format = "webp";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a request that passes through request headers
|
|
||||||
const imageRequest = new Request(imageURL, {
|
|
||||||
headers: request.headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Returning fetch() with resizing options will pass through response with the resized image.
|
|
||||||
const res = await fetch(imageRequest, { cf: { image: options } });
|
|
||||||
|
|
||||||
return new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, max-age=600",
|
|
||||||
"Content-Type": metadata.type,
|
|
||||||
"Content-Length": metadata.size.toString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user