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:
Classic298
2026-07-27 00:55:09 +02:00
committed by GitHub
parent bef63a2ae9
commit 69f8be4cf9
2 changed files with 49 additions and 25 deletions
+4 -2
View File
@@ -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 };
};
+45 -23
View File
@@ -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