Files
webstudio/apps/builder/app/routes/cgi.image.$.ts
T
Ivan Starkov 2dc2a21f4c feat: Support image transform for 3rd party images (#3585)
## Description

Now 3rd party images should be transformed by us

Corresponding SaaS PR
https://github.com/webstudio-is/webstudio-saas/pull/301

## Steps for reproduction

Instead of uploading asset set src to any 3rd party image url 
like `https://picsum.photos/id/237/200/300`

See it works.
Publish see it works.

Or open 


https://src-remote.staging.webstudio.is/builder/a1371dce-752c-4ccf-8ea4-88bab577fe50
See it works

https://new-images-saas-8njoh.wstd.work/



## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2024-06-18 19:38:23 +03:00

131 lines
3.8 KiB
TypeScript

import { createReadStream, existsSync } from "node:fs";
import { join } from "node:path";
import { createReadableStreamFromReadable } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import env from "~/env/env.server";
import { getImageNameAndType } from "~/builder/shared/assets/asset-utils";
import { z } from "zod";
import { createImageLoader } from "@webstudio-is/image";
const ImageParams = z.object({
width: z.string().transform((value) => Math.round(parseFloat(value))),
height: z.optional(
z.string().transform((value) => Math.round(parseFloat(value)))
),
fit: z.optional(z.literal("pad")),
background: z.optional(z.string()),
quality: z.string().transform((value) => Math.round(parseFloat(value))),
format: z.union([
z.literal("auto"),
z.literal("avif"),
z.literal("webp"),
z.literal("json"),
z.literal("jpeg"),
z.literal("png"),
z.literal("raw"),
]),
});
const decodePathFragment = (fragment: string) => {
return decodeURIComponent(fragment);
};
// this route used as proxy for images to cloudflare endpoint
// https://developers.cloudflare.com/fundamentals/get-started/reference/cdn-cgi-endpoint/
export const loader = async ({ request }: LoaderFunctionArgs) => {
const basePath = `/cgi/image/`;
const url = new URL(request.url);
const name = decodePathFragment(url.pathname.slice(basePath.length));
// The code should be +- same as here https://github.com/webstudio-is/webstudio-saas/blob/6c9a3bfb67cf5a221c20666de34bd20dc14bd558/packages/assets-proxy/src/image-transform.ts#L68
const rawParameters: Record<string, string> = {
format: url.searchParams.get("format") ?? "auto",
width: url.searchParams.get("width") ?? "0",
quality: url.searchParams.get("quality") ?? "80",
};
const height = url.searchParams.get("height");
if (height != null) {
rawParameters.height = height;
}
const fit = url.searchParams.get("fit");
if (fit != null) {
rawParameters.fit = fit;
}
const imageParameters = ImageParams.parse(rawParameters);
// Allow direct image access, and from the same origin
const refererRawUrl = request.headers.get("referer");
const refererUrl = refererRawUrl === null ? url : new URL(refererRawUrl);
if (refererUrl.origin !== url.origin) {
throw new Response("Forbidden", {
status: 403,
});
}
if (env.RESIZE_ORIGIN !== undefined) {
const imageLoader = createImageLoader({
imageBaseUrl: `${env.RESIZE_ORIGIN}/cgi/image/`,
});
const imgHref = imageLoader({
src: name,
...imageParameters,
format: "auto",
});
const imgUrl = new URL(imgHref);
imgUrl.search = url.search;
const response = await fetch(imgUrl.href, {
headers: {
accept: request.headers.get("accept") ?? "",
"accept-encoding": request.headers.get("accept-encoding") ?? "",
},
});
const responseWHeaders = new Response(response.body, response);
if (false === responseWHeaders.ok) {
console.error(
`Request to Image url ${imgUrl.href} responded with status = ${responseWHeaders.status}`
);
}
responseWHeaders.headers.set("Access-Control-Allow-Origin", url.origin);
return responseWHeaders;
}
if (env.FILE_UPLOAD_PATH === undefined) {
throw Error("FILE_UPLOAD_PATH is not provided");
}
const fileUploadPath = env.FILE_UPLOAD_PATH;
const filePath = join(process.cwd(), fileUploadPath, name);
if (existsSync(filePath) === false) {
throw new Response("Not found", {
status: 404,
});
}
const [contentType] = getImageNameAndType(name) ?? ["image/png"];
return new Response(
createReadableStreamFromReadable(createReadStream(filePath)),
{
headers: {
"content-type": contentType,
},
}
);
};