mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: show live progress while downloading named files
Bunny/cross-origin downloads are fetched into a blob before saving, which on large files or slow connections looked stuck (spinner only). Stream the body through a counting transform and show real byte progress in a toast: per-file percent for single downloads and file N/M + percent for bulk. - Progress is measured from Content-Length + received bytes (not estimated). - The blob is assembled by the browser from the stream (can be disk-backed), so we don't accumulate chunks in the JS heap. - Only the blob path shows a toast; same-origin (R2/S3/MinIO) and the >10 GB fallback use the browser's native download UI.
This commit is contained in:
@@ -4,6 +4,15 @@ import { downloadNamedFile, navigateDownload } from '@/lib/client/download-file'
|
||||
|
||||
const DOWNLOAD_STAGGER_MS = 500;
|
||||
|
||||
export type ManifestDownloadProgress = {
|
||||
/** 1-based index of the file currently downloading. */
|
||||
index: number;
|
||||
total: number;
|
||||
fileName: string;
|
||||
receivedBytes: number;
|
||||
totalBytes: number | null;
|
||||
};
|
||||
|
||||
export type ProjectDownloadManifestFile = {
|
||||
fileName: string;
|
||||
url: string;
|
||||
@@ -21,10 +30,14 @@ function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function triggerBrowserDownload(file: ProjectDownloadManifestFile): Promise<void> {
|
||||
async function triggerBrowserDownload(
|
||||
file: ProjectDownloadManifestFile,
|
||||
onProgress?: (received: number, total: number | null) => void
|
||||
): Promise<void> {
|
||||
// Same-origin proxy files (R2 / S3 / MinIO via /api/upload/video/...): the
|
||||
// download attribute applies and the browser streams straight to disk, so the
|
||||
// name is correct at any size with no memory cost.
|
||||
// name is correct at any size with no memory cost (browser shows its own
|
||||
// progress, so we don't track bytes here).
|
||||
if (file.url.startsWith('/api/upload/video/')) {
|
||||
navigateDownload(file.url, file.fileName);
|
||||
return;
|
||||
@@ -33,17 +46,39 @@ async function triggerBrowserDownload(file: ProjectDownloadManifestFile): Promis
|
||||
// Bunny (CDN redirect) and external direct hosts are cross-origin, so the name
|
||||
// only applies if we fetch the bytes. downloadNamedFile does that for files up
|
||||
// to 10 GB; larger ones fall back to a plain navigation (CDN filename).
|
||||
const saved = await downloadNamedFile(file.url, file.fileName);
|
||||
const saved = await downloadNamedFile(file.url, file.fileName, (p) =>
|
||||
onProgress?.(p.receivedBytes, p.totalBytes)
|
||||
);
|
||||
if (!saved) {
|
||||
navigateDownload(file.url, file.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runProjectDownloadManifest(manifest: ProjectDownloadManifest): Promise<void> {
|
||||
for (let index = 0; index < manifest.files.length; index += 1) {
|
||||
export async function runProjectDownloadManifest(
|
||||
manifest: ProjectDownloadManifest,
|
||||
onProgress?: (progress: ManifestDownloadProgress) => void
|
||||
): Promise<void> {
|
||||
const total = manifest.files.length;
|
||||
for (let index = 0; index < total; index += 1) {
|
||||
const file = manifest.files[index]!;
|
||||
onProgress?.({
|
||||
index: index + 1,
|
||||
total,
|
||||
fileName: file.fileName,
|
||||
receivedBytes: 0,
|
||||
totalBytes: file.sizeBytes,
|
||||
});
|
||||
// Sequential so at most one file is buffered in memory at a time.
|
||||
await triggerBrowserDownload(manifest.files[index]!);
|
||||
if (index < manifest.files.length - 1) {
|
||||
await triggerBrowserDownload(file, (received, fileTotal) =>
|
||||
onProgress?.({
|
||||
index: index + 1,
|
||||
total,
|
||||
fileName: file.fileName,
|
||||
receivedBytes: received,
|
||||
totalBytes: fileTotal,
|
||||
})
|
||||
);
|
||||
if (index < total - 1) {
|
||||
await sleep(DOWNLOAD_STAGGER_MS);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user