fix(download): warn before the tab closes mid-download

Bunny and direct downloads are pulled through fetch() so we can save them
under our own filename. The browser does not treat that as a download, so
closing the tab discarded everything received so far without a word.

Register a reference counted beforeunload guard while those transfers are
in flight, and while a project manifest is being pulled file by file.
Browser owned downloads (same-origin proxy, the over-10GB fallback, asset
downloads) survive a tab close on their own and stay unguarded.
This commit is contained in:
2026-08-22 12:50:45 +03:00
parent 74e4b4353e
commit cba8163286
5 changed files with 200 additions and 0 deletions
@@ -52,6 +52,7 @@ import {
type ProjectDownloadManifest,
} from '@/lib/client/project-download';
import { downloadProgressPercent } from '@/lib/client/download-file';
import { beginUnloadGuard } from '@/lib/client/unload-guard';
import {
createDownloadProgressToast,
type DownloadProgressToastHandle,
@@ -209,6 +210,7 @@ export function ProjectContentClient({
setIsDownloading(true);
let progressToast: DownloadProgressToastHandle | null = null;
let releaseUnloadGuard: (() => void) | null = null;
try {
const response = await fetch(`/api/projects/${projectId}/download${query}`, {
cache: 'no-store',
@@ -232,6 +234,9 @@ export function ProjectContentClient({
title: `Downloading ${manifest.totalFiles} files`,
description: 'Starting…',
});
// The files are pulled one by one through this tab, so closing it drops
// everything that hasn't been saved yet. Warn before that happens.
releaseUnloadGuard = beginUnloadGuard();
await runProjectDownloadManifest(manifest, (p) => {
const percent = downloadProgressPercent({
receivedBytes: p.receivedBytes,
@@ -250,6 +255,7 @@ export function ProjectContentClient({
progressToast?.dismiss();
toast.error('Failed to start project download');
} finally {
releaseUnloadGuard?.();
setIsDownloading(false);
}
},