mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 03:16:38 +00:00
71 lines
2.0 KiB
TypeScript
71 lines
2.0 KiB
TypeScript
import { createWriteStream, renameSync, unlinkSync } from "node:fs";
|
|
import { get as httpsGet } from "node:https";
|
|
import { pipeline } from "node:stream/promises";
|
|
|
|
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
|
|
export interface DownloadOptions {
|
|
/** Abort after this many milliseconds without network activity. */
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
function removePartialFile(path: string): void {
|
|
try {
|
|
unlinkSync(path);
|
|
} catch {
|
|
// Missing/locked partial files are handled by the next atomic download.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download a file from a URL, following redirects.
|
|
* Uses atomic write (download to .tmp, rename on success) to prevent
|
|
* corrupt partial files from persisting in the cache on interruption.
|
|
*/
|
|
export function downloadFile(
|
|
url: string,
|
|
dest: string,
|
|
options: DownloadOptions = {},
|
|
): Promise<void> {
|
|
const tmp = `${dest}.tmp`;
|
|
const timeoutMs = options.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS;
|
|
return new Promise((resolve, reject) => {
|
|
const follow = (u: string) => {
|
|
const request = httpsGet(u, (res) => {
|
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
const location = res.headers.location;
|
|
if (location) {
|
|
res.resume();
|
|
follow(location);
|
|
return;
|
|
}
|
|
}
|
|
if (res.statusCode !== 200) {
|
|
res.resume();
|
|
removePartialFile(tmp);
|
|
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
return;
|
|
}
|
|
const file = createWriteStream(tmp);
|
|
pipeline(res, file)
|
|
.then(() => {
|
|
renameSync(tmp, dest);
|
|
resolve();
|
|
})
|
|
.catch((err) => {
|
|
removePartialFile(tmp);
|
|
reject(err);
|
|
});
|
|
});
|
|
request.setTimeout(timeoutMs, () => {
|
|
request.destroy(new Error(`Download timed out after ${timeoutMs}ms`));
|
|
});
|
|
request.on("error", (err) => {
|
|
removePartialFile(tmp);
|
|
reject(err);
|
|
});
|
|
};
|
|
follow(url);
|
|
});
|
|
}
|