51 lines
3.0 KiB
TypeScript
51 lines
3.0 KiB
TypeScript
export type CanvasZipTaskView = {
|
|
id: string; status: "pending" | "running" | "completed" | "failed";
|
|
stage: string; progress: number; totalCount: number; successCount: number; failedCount: number;
|
|
error?: string; downloadUrl?: string;
|
|
};
|
|
const endpoint = "/api/canvas/download-zip-task";
|
|
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
async function taskRequest(url: string, init?: RequestInit) {
|
|
for (let attempt = 0; ; attempt++) {
|
|
let response: Response;
|
|
try { response = await fetch(url, { ...init, cache: "no-store", signal: AbortSignal.timeout(20_000) }); }
|
|
catch { if (attempt < 3) { await wait(1000 * (attempt + 1)); continue; } throw new Error("打包连接暂时中断,再次点击可继续查询原任务"); }
|
|
const payload = await response.json().catch(() => null);
|
|
if ((response.status === 429 || response.status >= 500) && attempt < 3) {
|
|
await wait(Math.min(30_000, Math.max(1000, Number(response.headers.get("retry-after") || 2) * 1000))); continue;
|
|
}
|
|
if (!response.ok || !payload?.task) throw new Error(payload?.error || "无法查询打包任务,请稍后重试");
|
|
return payload.task as CanvasZipTaskView;
|
|
}
|
|
}
|
|
export async function downloadCanvasSelectionZip(params: {
|
|
folderName: string; files: Array<{ url: string; name: string }>; selectionKey: string;
|
|
onProgress: (task: CanvasZipTaskView) => void;
|
|
}) {
|
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([params.selectionKey, params.files])));
|
|
const key = "canvas-zip:" + Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
let taskId = "";
|
|
try { taskId = sessionStorage.getItem(key) || ""; } catch { /* Storage is optional. */ }
|
|
if (!/^[0-9a-f-]{36}$/i.test(taskId)) taskId = crypto.randomUUID();
|
|
try { sessionStorage.setItem(key, taskId); } catch { /* Downloads work without browser persistence. */ }
|
|
let task = await taskRequest(endpoint, { method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ taskId, folderName: params.folderName, files: params.files, retry: true }) });
|
|
taskId = task.id;
|
|
try { sessionStorage.setItem(key, taskId); } catch {}
|
|
const deadline = Date.now() + 30 * 60_000;
|
|
for (;;) {
|
|
params.onProgress(task);
|
|
if (task.status === "failed") throw new Error(task.error || "打包失败,再次点击可重试");
|
|
if (task.status === "completed") break;
|
|
if (Date.now() > deadline) throw new Error("后台仍在准备图片,再次点击可继续查询进度");
|
|
await wait(2500);
|
|
task = await taskRequest(`${endpoint}?taskId=${encodeURIComponent(taskId)}`);
|
|
}
|
|
if (!task.successCount || !task.downloadUrl) throw new Error("没有成功准备的图片,请稍后重试");
|
|
// A GET for a complete immutable file supports browser retries and byte ranges.
|
|
const frame = document.createElement("iframe");
|
|
frame.hidden = true; frame.src = task.downloadUrl; document.body.appendChild(frame);
|
|
window.setTimeout(() => frame.remove(), 60 * 60_000);
|
|
return task;
|
|
}
|