fix: prevent canvas zip download from hanging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-19 21:34:11 +08:00
parent fa3460d719
commit e344a6db73
2 changed files with 77 additions and 39 deletions

View File

@ -10,6 +10,9 @@ type DownloadFileItem = {
name?: string;
};
const FETCH_TIMEOUT_MS = 15000;
const MAX_CONCURRENCY = 6;
function sanitizeFileName(value: string, fallback: string): string {
const cleaned = String(value || "")
.replace(/\.[a-zA-Z0-9]{2,6}$/g, "")
@ -46,6 +49,19 @@ function sanitizeFolderName(value: string): string {
return cleaned || `canvas-images-${Date.now()}`;
}
async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {
...init,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
function getRequestOrigin(request: NextRequest): string {
const forwardedHost = (request.headers.get("x-forwarded-host") || "").split(",")[0]?.trim();
const forwardedProto = (request.headers.get("x-forwarded-proto") || "").split(",")[0]?.trim();
@ -98,47 +114,58 @@ export async function POST(request: NextRequest) {
const zip = new JSZip();
const folder = zip.folder(folderName) || zip;
const cookie = request.headers.get("cookie") || "";
const results: Array<{ filename: string; bytes: Buffer } | null> = new Array(files.length).fill(null);
let cursor = 0;
const workerCount = Math.min(MAX_CONCURRENCY, files.length);
await Promise.all(
Array.from({ length: workerCount }, async () => {
while (true) {
const index = cursor;
cursor += 1;
if (index >= files.length) break;
const file = files[index]!;
try {
const targetOrigin = new URL(file.url).origin;
const headers =
targetOrigin === requestOrigin && cookie
? ({
cookie,
} as HeadersInit)
: undefined;
const response = await fetchWithTimeout(
file.url,
{
method: "GET",
cache: "no-store",
headers,
},
FETCH_TIMEOUT_MS
);
if (!response.ok) continue;
const ext = inferExt(file.url, response.headers.get("content-type") || "");
const baseName = sanitizeFileName(file.name || `canvas-image-${index + 1}`, `canvas-image-${index + 1}`);
const filename = `${baseName}-${index + 1}.${ext}`;
const bytes = Buffer.from(await response.arrayBuffer());
results[index] = { filename, bytes };
} catch (_error) {
continue;
}
}
})
);
let successCount = 0;
let failedCount = 0;
const usedNames = new Set<string>();
const cookie = request.headers.get("cookie") || "";
for (let index = 0; index < files.length; index += 1) {
const file = files[index]!;
try {
const targetOrigin = new URL(file.url).origin;
const headers =
targetOrigin === requestOrigin && cookie
? ({
cookie,
} as HeadersInit)
: undefined;
const response = await fetch(file.url, {
method: "GET",
cache: "no-store",
headers,
});
if (!response.ok) {
failedCount += 1;
continue;
}
const ext = inferExt(file.url, response.headers.get("content-type") || "");
const baseName = sanitizeFileName(file.name || `canvas-image-${index + 1}`, `canvas-image-${index + 1}`);
let candidate = `${baseName}.${ext}`;
let suffix = 2;
while (usedNames.has(candidate)) {
candidate = `${baseName}-${suffix}.${ext}`;
suffix += 1;
}
usedNames.add(candidate);
const bytes = Buffer.from(await response.arrayBuffer());
folder.file(candidate, bytes);
successCount += 1;
} catch (_error) {
for (const item of results) {
if (!item) {
failedCount += 1;
continue;
}
folder.file(item.filename, item.bytes);
successCount += 1;
}
if (successCount === 0) {

View File

@ -563,12 +563,16 @@ export function CanvasWorkspace() {
}
setIsDownloadSubmitting(true);
let timeoutId: number | null = null;
try {
const controller = new AbortController();
timeoutId = window.setTimeout(() => controller.abort(), 120000);
const response = await fetch("/api/canvas/download-zip", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
body: JSON.stringify({
folderName,
files,
@ -593,9 +597,16 @@ export function CanvasWorkspace() {
} else {
toast.success(`已下载压缩包,包含 ${successCount || totalCount} 张图片`);
}
} catch (_error) {
toast.error("下载失败,请稍后重试");
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
toast.error("打包超时,请减少图片数量后重试");
} else {
toast.error("下载失败,请稍后重试");
}
} finally {
if (timeoutId) {
window.clearTimeout(timeoutId);
}
setIsDownloadSubmitting(false);
}
}, [isDownloadSubmitting, selectedImageNodes]);