fix: prevent canvas zip download from hanging
This commit is contained in:
parent
fa3460d719
commit
e344a6db73
@ -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) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user