Files
webstudio/apps/builder/app/routes/cgi.image.$.ts
Oleg Isonen 82c3d83fd1 fix: optimize CLI import for large data amount (#5831)
## Summary

- Add CLI project import support from synced `.webstudio/data.json` into
another project via a destination share link.
- Move project bundle import/export contract into
`@webstudio-is/protocol`, including bundle versioning, staged upload
constants, schema validation, and fixture coverage.
- Upload project bundle data through staged TUS upload before calling
the import RPC to avoid request body limits.
- Upload referenced assets as binary files through the existing asset
upload route before importing asset rows.
- Add `--skip-assets` and `--ignore-version-check` import options.
- Add retry handling for transient missing imported asset files by
re-uploading only the reported missing assets.
- Share missing-asset import error formatting/parsing through protocol
to avoid duplicated string contracts.
- Preserve browser asset upload behavior while adding API upload
support.
- Normalize schema naming across the branch to lower-case names without
`Schema` suffix.
2026-06-23 18:50:01 +02:00

125 lines
3.6 KiB
TypeScript

import { createReadStream, existsSync } from "node:fs";
import { join } from "node:path";
import { z } from "zod";
import { createReadableStreamFromReadable } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { wsImageLoader } from "@webstudio-is/image";
import { decodePathFragment } from "@webstudio-is/sdk";
import env from "~/env/env.server";
import { getImageNameAndType } from "~/builder/shared/assets/asset-utils";
import { fileUploadPath } from "~/shared/asset-client";
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"),
]),
});
// 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.host !== url.host) {
throw new Response("Forbidden", {
status: 403,
});
}
if (env.RESIZE_ORIGIN !== undefined) {
const imgHref = wsImageLoader({
src: name,
...imageParameters,
format: "auto",
});
const imgUrl = new URL(env.RESIZE_ORIGIN + 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;
}
// support absolute urls locally
if (URL.canParse(name)) {
return fetch(name);
}
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,
},
}
);
};