79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
|
|
type SaveFilePicker = (options?: {
|
||
|
|
suggestedName?: string;
|
||
|
|
types?: Array<{
|
||
|
|
description?: string;
|
||
|
|
accept: Record<string, string[]>;
|
||
|
|
}>;
|
||
|
|
}) => Promise<{
|
||
|
|
createWritable: () => Promise<{
|
||
|
|
write: (data: Blob) => Promise<void>;
|
||
|
|
close: () => Promise<void>;
|
||
|
|
}>;
|
||
|
|
}>;
|
||
|
|
|
||
|
|
type NavigatorWithFileShare = Navigator & {
|
||
|
|
canShare?: (data: { files?: File[] }) => boolean;
|
||
|
|
share?: (data: { files?: File[]; title?: string }) => Promise<void>;
|
||
|
|
};
|
||
|
|
|
||
|
|
type WindowWithSavePicker = Window & {
|
||
|
|
showSaveFilePicker?: SaveFilePicker;
|
||
|
|
};
|
||
|
|
|
||
|
|
export async function downloadFile(url: string, filename: string) {
|
||
|
|
const res = await fetch(url, { credentials: "include" });
|
||
|
|
if (!res.ok) throw new Error(await res.text());
|
||
|
|
|
||
|
|
const blob = await res.blob();
|
||
|
|
const safeName = filename || "download";
|
||
|
|
|
||
|
|
if (window.isSecureContext) {
|
||
|
|
const picker = (window as WindowWithSavePicker).showSaveFilePicker;
|
||
|
|
if (picker) {
|
||
|
|
const handle = await picker({
|
||
|
|
suggestedName: safeName,
|
||
|
|
types: blob.type
|
||
|
|
? [
|
||
|
|
{
|
||
|
|
description: "File",
|
||
|
|
accept: { [blob.type]: [extensionFromName(safeName)] },
|
||
|
|
},
|
||
|
|
]
|
||
|
|
: undefined,
|
||
|
|
});
|
||
|
|
const writable = await handle.createWritable();
|
||
|
|
await writable.write(blob);
|
||
|
|
await writable.close();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const file = new File([blob], safeName, {
|
||
|
|
type: blob.type || "application/octet-stream",
|
||
|
|
});
|
||
|
|
const nav = navigator as NavigatorWithFileShare;
|
||
|
|
if (nav.canShare?.({ files: [file] }) && nav.share) {
|
||
|
|
await nav.share({ files: [file], title: safeName });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const objectUrl = URL.createObjectURL(blob);
|
||
|
|
triggerDownload(objectUrl, safeName);
|
||
|
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function triggerDownload(url: string, filename: string) {
|
||
|
|
const a = document.createElement("a");
|
||
|
|
a.href = url;
|
||
|
|
a.download = filename;
|
||
|
|
a.style.display = "none";
|
||
|
|
document.body.append(a);
|
||
|
|
a.click();
|
||
|
|
a.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
function extensionFromName(filename: string) {
|
||
|
|
const match = /\.[^.]+$/.exec(filename);
|
||
|
|
return match?.[0] || ".bin";
|
||
|
|
}
|