mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-18 12:18:29 -07:00
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import { execFile as defaultExecFile } from 'node:child_process';
|
|
import type { FetchLike, FetchResponseLike } from './release-assets';
|
|
import type { CurlExecFile } from './curl-http-executor';
|
|
|
|
export interface ElectronNetFetchLike {
|
|
fetch: (url: string, init?: Record<string, unknown>) => Promise<FetchResponseLike>;
|
|
}
|
|
|
|
export type GlobalFetchLike = (url: string, init?: RequestInit) => Promise<FetchResponseLike>;
|
|
|
|
export function createElectronNetFetch(net: ElectronNetFetchLike): FetchLike {
|
|
return (url, init) => net.fetch(url, init);
|
|
}
|
|
|
|
function getGlobalFetch(): GlobalFetchLike {
|
|
if (typeof globalThis.fetch !== 'function') {
|
|
throw new Error('Global fetch is not available for updater requests.');
|
|
}
|
|
return globalThis.fetch.bind(globalThis) as GlobalFetchLike;
|
|
}
|
|
|
|
export function createGlobalFetch(fetchImpl?: GlobalFetchLike): FetchLike {
|
|
return (url, init) => (fetchImpl ?? getGlobalFetch())(url, init as RequestInit);
|
|
}
|
|
|
|
type CurlFetchOptions = {
|
|
execFile?: CurlExecFile;
|
|
curlPath?: string;
|
|
};
|
|
|
|
function addHeaderArgs(args: string[], headers: unknown): void {
|
|
if (!headers) return;
|
|
if (Array.isArray(headers)) {
|
|
for (const header of headers) {
|
|
if (Array.isArray(header) && header.length >= 2) {
|
|
args.push('--header', `${header[0]}: ${header[1]}`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (typeof headers === 'object' && 'forEach' in headers) {
|
|
(headers as { forEach: (callback: (value: string, name: string) => void) => void }).forEach(
|
|
(value, name) => {
|
|
args.push('--header', `${name}: ${value}`);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
if (typeof headers !== 'object') return;
|
|
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
if (value === undefined) continue;
|
|
const values = Array.isArray(value) ? value : [value];
|
|
for (const item of values) {
|
|
args.push('--header', `${name}: ${String(item)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
|
|
const result = new ArrayBuffer(buffer.length);
|
|
new Uint8Array(result).set(buffer);
|
|
return result;
|
|
}
|
|
|
|
export function createCurlFetch(options: CurlFetchOptions = {}): FetchLike {
|
|
const execFile = options.execFile ?? (defaultExecFile as unknown as CurlExecFile);
|
|
const curlPath = options.curlPath ?? '/usr/bin/curl';
|
|
|
|
return async (url, init = {}) => {
|
|
const args = [
|
|
'--fail',
|
|
'--location',
|
|
'--silent',
|
|
'--show-error',
|
|
'--connect-timeout',
|
|
'30',
|
|
'--max-time',
|
|
'60',
|
|
];
|
|
addHeaderArgs(args, init.headers);
|
|
args.push(url);
|
|
// curl has its own --max-time, but a caller-supplied signal must be able to
|
|
// cut the request short of it; otherwise a shorter caller deadline is a lie.
|
|
const signal = init.signal instanceof AbortSignal ? init.signal : undefined;
|
|
// Mirror fetch: reject with the caller's reason whatever its type, and only
|
|
// synthesise an error when the signal carries none.
|
|
const abortReason = (): unknown =>
|
|
signal?.reason !== undefined ? signal.reason : new Error('curl request aborted');
|
|
if (signal?.aborted) throw abortReason();
|
|
|
|
const body = await new Promise<Buffer>((resolve, reject) => {
|
|
let onAbort: (() => void) | null = null;
|
|
const settle = (run: () => void) => {
|
|
if (onAbort) signal?.removeEventListener('abort', onAbort);
|
|
onAbort = null;
|
|
run();
|
|
};
|
|
|
|
const child = execFile(
|
|
curlPath,
|
|
args,
|
|
{
|
|
encoding: 'buffer',
|
|
maxBuffer: 600 * 1024 * 1024,
|
|
timeout: 65_000,
|
|
},
|
|
(error, stdout, stderr) => {
|
|
if (error) {
|
|
const stderrMessage = Buffer.isBuffer(stderr) ? stderr.toString('utf8') : stderr;
|
|
const errno = (error as NodeJS.ErrnoException).code;
|
|
const fallback = errno ? `curl failed (${errno})` : 'curl failed';
|
|
settle(() => reject(new Error(stderrMessage.trim() || fallback)));
|
|
return;
|
|
}
|
|
settle(() => resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout)));
|
|
},
|
|
);
|
|
|
|
if (signal) {
|
|
onAbort = () => {
|
|
onAbort = null;
|
|
child.kill('SIGKILL');
|
|
reject(abortReason());
|
|
};
|
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
}
|
|
});
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
json: async () => JSON.parse(body.toString('utf8')),
|
|
text: async () => body.toString('utf8'),
|
|
arrayBuffer: async () => bufferToArrayBuffer(body),
|
|
};
|
|
};
|
|
}
|