Files
Arkie-Library-Frontend/src/components/messageStream/utils/downloadFile.ts

102 lines
3.1 KiB
TypeScript
Raw Normal View History

import { assetUrl } from "../../../api";
import { isInAppBrowser } from "../../../utils/inAppBrowser";
export const IN_APP_DOWNLOAD_GUIDE_EVENT = "ark:in-app-download-guide";
export type InAppDownloadGuideDetail = { url: string; filename: string };
/**
* Files larger than this skip the fetchBlob path so mobile WebViews and
* memory-constrained devices do not OOM. They fall back to the anchor
* download (relies on the backend's `Content-Disposition: attachment`).
*/
const MAX_BLOB_DOWNLOAD_BYTES = 50 * 1024 * 1024;
export type DownloadOptions = { sizeBytes?: number };
2026-06-01 23:00:28 +08:00
export function pauseActiveVideos() {
document.querySelectorAll("video").forEach((video) => {
video.pause();
});
}
export function attachmentDownloadUrl(postId: string, attachmentId: string) {
return assetUrl(
`/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent(
attachmentId,
)}/download`,
);
}
export async function downloadAttachment(
postId: string,
attachmentId: string,
filename: string,
options?: DownloadOptions,
) {
return downloadFile(
attachmentDownloadUrl(postId, attachmentId),
filename,
options,
);
}
export async function downloadFile(
url: string,
filename: string,
options?: DownloadOptions,
) {
const safeFilename = filename || "download";
// In-app WebViews (WeChat / TokenPocket / Telegram / iOS WKWebView / …)
// ignore `Content-Disposition: attachment` and have no system download
// manager, so an anchor click would just open the file inline. Surface an
// "open in external browser" guide instead of silently failing the user.
if (typeof window !== "undefined" && isInAppBrowser()) {
window.dispatchEvent(
new CustomEvent<InAppDownloadGuideDetail>(IN_APP_DOWNLOAD_GUIDE_EVENT, {
detail: { url, filename: safeFilename },
}),
);
return;
}
// Normal browsers: prefer fetch → Blob → object URL so the file always lands
// in the Downloads folder with the original filename, even when the browser
// would otherwise inline-preview the response (Chrome and Safari do this for
// PDFs / images regardless of Content-Disposition). Fall back to the anchor
// download for large files (avoid loading them entirely into memory) or when
// fetch fails for any reason.
const sizeBytes = options?.sizeBytes ?? 0;
if (sizeBytes <= MAX_BLOB_DOWNLOAD_BYTES) {
try {
const res = await fetch(url, { credentials: "omit" });
if (res.ok) {
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
try {
triggerDownload(objectUrl, safeFilename);
} finally {
// Give the browser a moment to start the download before revoking.
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 4000);
}
return;
}
} catch {
// fall through to anchor fallback
}
}
triggerDownload(url, safeFilename);
}
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();
}