Files
reverse-engineering-anything/src/application/HopperDownload.ts
T
morluto a01148553e fix(setup): preserve recovery backups and retain installer cleanup
Validation: integrated changes passed formatting, typecheck, lint, resolved module-boundary verification and git diff --check. Tests and real-provider workflows were not run.
2026-10-11 00:03:21 +08:00

44 lines
1.5 KiB
TypeScript

/** Maximum admitted vendor response bytes, shared by Hopper setup adapters. */
export const HOPPER_DOWNLOAD_BYTES = 100_000_000;
/** Bound observed response bytes before retaining them, including chunked bodies. */
export const downloadHopperPackage = async (
url: string,
options: { readonly signal?: AbortSignal } = {},
): Promise<{ readonly ok: boolean; readonly bytes: Uint8Array }> => {
const deadline = AbortSignal.timeout(120_000);
const response = await fetch(url, {
headers: { accept: "application/json", "user-agent": "rea-installer" },
signal:
options.signal === undefined
? deadline
: AbortSignal.any([options.signal, deadline]),
});
const refused = { ok: false, bytes: new Uint8Array() };
if (
!response.ok ||
Number(response.headers.get("content-length")) > HOPPER_DOWNLOAD_BYTES
) {
await response.body?.cancel();
return refused;
}
if (response.body === null) return { ok: true, bytes: new Uint8Array() };
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let bytes = 0;
try {
while (true) {
const chunk = await reader.read();
if (chunk.done) return { ok: true, bytes: Buffer.concat(chunks, bytes) };
if (chunk.value.byteLength > HOPPER_DOWNLOAD_BYTES - bytes) {
await reader.cancel();
return refused;
}
chunks.push(chunk.value);
bytes += chunk.value.byteLength;
}
} finally {
reader.releaseLock();
}
};