From 867c52c24f23af6d918ddb40e97e2e24c42d8607 Mon Sep 17 00:00:00 2001 From: yuanzonghao Date: Sun, 7 Jun 2026 22:32:23 +0800 Subject: [PATCH] fix(gallery): address review findings in zip download module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle downloadImagesAsZip return value and surface errors to user - Fix inferImageExtension garbage output for data URIs without semicolons - Scale blob URL revocation delay for large zip files (>5MB → 60s) - Cap uniqueZipPath dedup loop at 10k iterations with timestamp fallback - Support relative URLs in inferImageExtension via base URL - Handle svg+xml MIME subtype correctly Co-Authored-By: Claude Opus 4.6 --- app/gallery/page.tsx | 9 ++++++++- lib/imageZipDownload.ts | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/gallery/page.tsx b/app/gallery/page.tsx index 5dd2aa2..d79361f 100644 --- a/app/gallery/page.tsx +++ b/app/gallery/page.tsx @@ -858,7 +858,14 @@ function GalleryInner() { name: `infiplot-branch-${String(branchN).padStart(3, "0")}.${inferImageExtension(alt.imageUrl)}`, }); } - await downloadImagesAsZip(files, `infiplot-gallery-${doc.id}.zip`); + const result = await downloadImagesAsZip(files, `infiplot-gallery-${doc.id}.zip`); + if (result.downloaded === 0) { + alert("所有图片抓取失败,请检查网络后重试"); + } else if (result.failed.length > 0) { + alert(`已打包 ${result.downloaded} 张,${result.failed.length} 张抓取失败`); + } + } catch { + alert("打包下载失败,请重试"); } finally { setDownloadingScenes(false); } diff --git a/lib/imageZipDownload.ts b/lib/imageZipDownload.ts index f6acabf..ba5a00a 100644 --- a/lib/imageZipDownload.ts +++ b/lib/imageZipDownload.ts @@ -19,13 +19,17 @@ const DEFAULT_CONCURRENCY = 4; const DEFAULT_TIMEOUT_MS = 20_000; export function inferImageExtension(url: string): string { - if (url.startsWith("data:image/svg")) return "svg"; - if (url.startsWith("data:image/")) { - return url.slice(11, url.indexOf(";")).replace(/^jpeg$/, "jpg") || "png"; + const dataMatch = /^data:image\/([^;,]+)/i.exec(url); + if (dataMatch?.[1]) { + const sub = dataMatch[1].toLowerCase(); + if (sub === "svg+xml") return "svg"; + return sub === "jpeg" ? "jpg" : sub; } try { - const ext = new URL(url).pathname.split(".").pop()?.toLowerCase(); + const base = + typeof window !== "undefined" ? window.location.href : "http://localhost"; + const ext = new URL(url, base).pathname.split(".").pop()?.toLowerCase(); if (ext && ["jpg", "jpeg", "png", "webp", "gif", "svg"].includes(ext)) { return ext === "jpeg" ? "jpg" : ext; } @@ -143,7 +147,8 @@ function triggerBrowserDownload(blob: Blob, fileName: string): void { document.body.appendChild(a); a.click(); a.remove(); - setTimeout(() => URL.revokeObjectURL(blobUrl), 1500); + const delayMs = blob.size > 5_000_000 ? 60_000 : 1_500; + setTimeout(() => URL.revokeObjectURL(blobUrl), delayMs); } function normalizeZipName(name: string): string { @@ -161,15 +166,16 @@ function uniqueZipPath(name: string, usedPaths: Set): string { const dot = clean.lastIndexOf("."); const base = dot > 0 ? clean.slice(0, dot) : clean; const ext = dot > 0 ? clean.slice(dot) : ""; - let n = 2; - while (true) { + for (let n = 2; n < 10_000; n++) { const candidate = `${base}-${n}${ext}`; if (!usedPaths.has(candidate)) { usedPaths.add(candidate); return candidate; } - n++; } + const fallback = `${base}-${Date.now()}${ext}`; + usedPaths.add(fallback); + return fallback; } function sanitizeZipPath(name: string): string {