30 lines
2.6 KiB
TypeScript
30 lines
2.6 KiB
TypeScript
/**
|
|
* 旧画布的图片上传辅助,与新版canvas-service/assets的冻结资源协议分开保留。
|
|
* 同一次调用的重试沿用FormData和幂等键;这里只返回旧版素材记录,不维护可恢复上传账本。
|
|
*/
|
|
/** 由应用注入JSON/FormData请求端口,凭证与错误形状沿用宿主API封装。 */
|
|
type Request = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
/** 旧上传接口的素材响应形状,原件规范URL与尺寸由返回前校验。 */
|
|
export type CanvasAsset = { id: string; url: string; width: number; height: number; bytes: number; mime_type: string };
|
|
/** 上传原文件大小上限20MiB,空文件同样拒绝。 */
|
|
export const ASSET_LIMIT = 20 * 1024 * 1024;
|
|
/** 仅网络TypeError或5xx且未取消时追加一次同键重试;其它错误直接交给调用方。
|
|
* 调用方可提供key以跨尝试复用;默认key只在这次函数调用开始时生成,不是持久恢复记录。 */
|
|
export async function uploadCanvasImage(request: Request, workspace: string, file: Blob, name: string, signal?: AbortSignal, key = crypto.randomUUID()): Promise<CanvasAsset> {
|
|
if (!file.size || file.size > ASSET_LIMIT) throw new Error("asset_too_large");
|
|
const form = new FormData(); form.append("image", file, name);
|
|
/** 两次发送共用同一字节、文件名和key,避免丢响应后新建第二份素材。 */
|
|
const send = () => request<CanvasAsset>(`workspaces/${encodeURIComponent(workspace)}/assets`, { method: "POST", headers: { "Idempotency-Key": key }, body: form, signal });
|
|
let asset: CanvasAsset;
|
|
try { asset = await send(); } catch (error) {
|
|
if (signal?.aborted || !(error instanceof TypeError || typeof error === "object" && error && "status" in error && Number(error.status) >= 500)) throw error;
|
|
asset = await send(); // The same bytes/key retry a possibly committed upload.
|
|
}
|
|
// 成功HTTP不等于合法素材,继续核对ID、规范源站/路径及正有限尺寸,不接受任意远端URL。
|
|
if (!asset || typeof asset.url !== "string") throw new Error("invalid_asset_response");
|
|
let url: URL;
|
|
try { url = new URL(asset.url); } catch { throw new Error("invalid_asset_response"); }
|
|
if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(asset.id) || url.origin !== "https://api.foundry.entroia.com" || url.pathname !== `/projects/${process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap"}/workspaces/${workspace}/assets/${asset.id}` || url.search || !Number.isFinite(asset.width) || asset.width < 1 || !Number.isFinite(asset.height) || asset.height < 1) throw new Error("invalid_asset_response");
|
|
return asset;
|
|
}
|