From 69f8be4cf9224e4bfdef820800b3ab3878ebfbf8 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:55:09 +0200 Subject: [PATCH] fix: show download preparation toast and prevent duplicate zip jobs (#27421) * fix: show download preparation toast and prevent duplicate zip jobs Downloading a file or folder from the file navigator gave no feedback while the server prepared the response, which can take 30 seconds or more for large folders that are zipped server-side. Users assumed the click did nothing and pressed Download again, starting additional zip jobs on the server. Both downloadFile and bulkDownload, the two functions every download control funnels through, now show a persistent "Preparing download..." loading toast while the request is in flight and dismiss it once the download starts or fails. A shared downloading flag makes repeated clicks no-ops until the current download finishes, so a single click starts exactly one server-side job. Fixes #27055 * fix: report terminal download failures instead of dismissing the toast A failed download dismissed the preparation toast without saying anything, which reads as the download silently disappearing. Both download paths now report the failure. The two helpers also declare a nullable return but could still reject once the response body started streaming, so an interrupted transfer escaped as an unhandled rejection and left the same silent dismissal. They now return null in that case, which also stops an interrupted preview from leaving its spinner running. --- src/lib/apis/terminal/index.ts | 6 ++- src/lib/components/chat/FileNav.svelte | 68 +++++++++++++++++--------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index ee38a9a8fa..eedbcc3eed 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -143,7 +143,8 @@ export const downloadFileBlob = async ( if (!res || !res.ok) return null; const filename = path.split('/').pop() ?? 'file'; - const blob = await res.blob(); + const blob = await res.blob().catch(() => null); + if (!blob) return null; return { blob, filename }; }; @@ -170,7 +171,8 @@ export const archiveFromTerminal = async ( const disposition = res.headers.get('content-disposition') ?? ''; const match = disposition.match(/filename="?([^"]+)"?/); const filename = match?.[1] ?? 'download.zip'; - const blob = await res.blob(); + const blob = await res.blob().catch(() => null); + if (!blob) return null; return { blob, filename }; }; diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index b6f2102ace..86c3df609f 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -521,22 +521,34 @@ fileLoading = false; }; + let downloading = false; + const downloadFile = async (path: string) => { const terminal = selectedTerminal; - if (!terminal) return; + if (!terminal || downloading) return; - // Directories end with '/' — download as ZIP archive - const isDir = path.endsWith('/'); - const result = isDir - ? await archiveFromTerminal(terminal.url, terminal.key, [path.replace(/\/$/, '')]) - : await downloadFileBlob(terminal.url, terminal.key, path, chatId ?? undefined); - if (!result) return; - const url = URL.createObjectURL(result.blob); - const a = document.createElement('a'); - a.href = url; - a.download = result.filename; - a.click(); - URL.revokeObjectURL(url); + downloading = true; + const toastId = toast.loading($i18n.t('Preparing download...')); + try { + // Directories end with '/', downloaded as a ZIP archive + const isDir = path.endsWith('/'); + const result = isDir + ? await archiveFromTerminal(terminal.url, terminal.key, [path.replace(/\/$/, '')]) + : await downloadFileBlob(terminal.url, terminal.key, path, chatId ?? undefined); + if (!result) { + toast.error($i18n.t('Download failed')); + return; + } + const url = URL.createObjectURL(result.blob); + const a = document.createElement('a'); + a.href = url; + a.download = result.filename; + a.click(); + URL.revokeObjectURL(url); + } finally { + toast.dismiss(toastId); + downloading = false; + } }; // ── Drag-and-drop upload ───────────────────────────────────────────── @@ -785,7 +797,7 @@ const bulkDownload = async () => { const terminal = selectedTerminal; - if (!terminal) return; + if (!terminal || downloading) return; const paths = [...selectedEntries].map((p) => p.replace(/\/$/, '')); if (paths.length === 0) return; @@ -796,15 +808,25 @@ return; } - // Archive everything into a single ZIP - const result = await archiveFromTerminal(terminal.url, terminal.key, paths); - if (!result) return; - const url = URL.createObjectURL(result.blob); - const a = document.createElement('a'); - a.href = url; - a.download = result.filename; - a.click(); - URL.revokeObjectURL(url); + downloading = true; + const toastId = toast.loading($i18n.t('Preparing download...')); + try { + // Archive everything into a single ZIP + const result = await archiveFromTerminal(terminal.url, terminal.key, paths); + if (!result) { + toast.error($i18n.t('Download failed')); + return; + } + const url = URL.createObjectURL(result.blob); + const a = document.createElement('a'); + a.href = url; + a.download = result.filename; + a.click(); + URL.revokeObjectURL(url); + } finally { + toast.dismiss(toastId); + downloading = false; + } }; // Escape to clear selection