mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 16:47:54 +09:00
## 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.
125 lines
3.6 KiB
TypeScript
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,
|
|
},
|
|
}
|
|
);
|
|
};
|