mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
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.
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user