2026-07-05 14:49:09 -05:00
|
|
|
// Shared blob download: fetches an endpoint and saves the response as a file.
|
|
|
|
|
// Works whether or not the response sets Content-Disposition (some exports
|
|
|
|
|
// don't), falling back to the given name. Throws on a non-OK response so callers
|
|
|
|
|
// can surface the failure with a toast.
|
|
|
|
|
export async function downloadFile(endpoint: string, fallbackName: string): Promise<void> {
|
|
|
|
|
const res = await fetch(endpoint, { credentials: 'include' });
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
let data: { message?: string; error?: string } = {};
|
2026-07-06 14:23:53 -05:00
|
|
|
try {
|
|
|
|
|
data = await res.json();
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
2026-07-05 14:49:09 -05:00
|
|
|
throw new Error(data.message || data.error || `HTTP ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
const disposition = res.headers.get('Content-Disposition');
|
|
|
|
|
const match = disposition?.match(/filename="?([^"]+)"?/i);
|
|
|
|
|
const name = match?.[1] || fallbackName;
|
|
|
|
|
const blob = await res.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
2026-07-06 14:23:53 -05:00
|
|
|
a.href = url;
|
|
|
|
|
a.download = name;
|
|
|
|
|
a.click();
|
2026-07-05 14:49:09 -05:00
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|