import { CancellationToken, configureRequestUrl, newError, safeStringifyJson, UpdateFileInfo, UpdateInfo, WindowsUpdateInfo } from "builder-util-runtime" import { OutgoingHttpHeaders, RequestOptions } from "http" import { load } from "js-yaml" import { URL } from "url" import { ElectronHttpExecutor } from "../electronHttpExecutor" import { ResolvedUpdateFileInfo } from "../types" import { newUrlFromBase } from "../util" // @ts-ignore import * as escapeRegExp from "lodash.escaperegexp" export type ProviderPlatform = "darwin" | "linux" | "win32" export interface ProviderRuntimeOptions { isUseMultipleRangeRequest: boolean platform: ProviderPlatform executor: ElectronHttpExecutor } export abstract class Provider { private requestHeaders: OutgoingHttpHeaders | null = null protected readonly executor: ElectronHttpExecutor protected constructor(private readonly runtimeOptions: ProviderRuntimeOptions) { this.executor = runtimeOptions.executor } // By default, the blockmap file is in the same directory as the main file // But some providers may have a different blockmap file, so we need to override this method getBlockMapFiles(baseUrl: URL, oldVersion: string, newVersion: string, oldBlockMapFileBaseUrl: string | null = null): URL[] | Promise { const newBlockMapUrl = newUrlFromBase(`${baseUrl.pathname}.blockmap`, baseUrl) const oldBlockMapUrl = newUrlFromBase( `${baseUrl.pathname.replace(new RegExp(escapeRegExp(newVersion), "g"), oldVersion)}.blockmap`, oldBlockMapFileBaseUrl ? new URL(oldBlockMapFileBaseUrl) : baseUrl ) return [oldBlockMapUrl, newBlockMapUrl] } get isUseMultipleRangeRequest(): boolean { return this.runtimeOptions.isUseMultipleRangeRequest !== false } private getChannelFilePrefix(): string { if (this.runtimeOptions.platform === "linux") { const arch = process.env["TEST_UPDATER_ARCH"] || process.arch const archSuffix = arch === "x64" ? "" : `-${arch}` return "-linux" + archSuffix } else { return this.runtimeOptions.platform === "darwin" ? "-mac" : "" } } // due to historical reasons for windows we use channel name without platform specifier protected getDefaultChannelName(): string { return this.getCustomChannelName("latest") } protected getCustomChannelName(channel: string): string { return `${channel}${this.getChannelFilePrefix()}` } get fileExtraDownloadHeaders(): OutgoingHttpHeaders | null { return null } setRequestHeaders(value: OutgoingHttpHeaders | null): void { this.requestHeaders = value } abstract getLatestVersion(): Promise abstract resolveFiles(updateInfo: T): Array /** * Method to perform API request only to resolve update info, but not to download update. */ protected httpRequest(url: URL, headers?: OutgoingHttpHeaders | null, cancellationToken?: CancellationToken): Promise { return this.executor.request(this.createRequestOptions(url, headers), cancellationToken) } protected createRequestOptions(url: URL, headers?: OutgoingHttpHeaders | null): RequestOptions { const result: RequestOptions = {} if (this.requestHeaders == null) { if (headers != null) { result.headers = headers } } else { result.headers = headers == null ? this.requestHeaders : { ...this.requestHeaders, ...headers } } configureRequestUrl(url, result) return result } } export function findFile(files: Array, extension: string, not?: Array): ResolvedUpdateFileInfo | null | undefined { if (files.length === 0) { throw newError("No files provided", "ERR_UPDATER_NO_FILES_PROVIDED") } const filteredFiles = files.filter(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`)) const result = filteredFiles.find(it => [it.url.pathname, it.info.url].some(n => n.includes(process.arch))) ?? filteredFiles.shift() if (result) { return result } else if (not == null) { return files[0] } else { return files.find(fileInfo => !not.some(ext => fileInfo.url.pathname.toLowerCase().endsWith(`.${ext.toLowerCase()}`))) } } export function parseUpdateInfo(rawData: string | null, channelFile: string, channelFileUrl: URL): UpdateInfo { if (rawData == null) { throw newError(`Cannot parse update info from ${channelFile} in the latest release artifacts (${channelFileUrl}): rawData: null`, "ERR_UPDATER_INVALID_UPDATE_INFO") } let result: UpdateInfo try { result = load(rawData) as UpdateInfo } catch (e: any) { throw newError( `Cannot parse update info from ${channelFile} in the latest release artifacts (${channelFileUrl}): ${e.stack || e.message}, rawData: ${rawData}`, "ERR_UPDATER_INVALID_UPDATE_INFO" ) } return result } export function getFileList(updateInfo: UpdateInfo): Array { const files = updateInfo.files if (files != null && files.length > 0) { return files } // noinspection JSDeprecatedSymbols if (updateInfo.path != null) { // noinspection JSDeprecatedSymbols return [ { url: updateInfo.path, sha2: (updateInfo as any).sha2, sha512: updateInfo.sha512, } as any, ] } else { throw newError(`No files provided: ${safeStringifyJson(updateInfo)}`, "ERR_UPDATER_NO_FILES_PROVIDED") } } export function resolveFiles(updateInfo: UpdateInfo, baseUrl: URL, pathTransformer: (p: string) => string = (p: string): string => p): Array { const files = getFileList(updateInfo) const result: Array = files.map(fileInfo => { if ((fileInfo as any).sha2 == null && fileInfo.sha512 == null) { throw newError(`Update info doesn't contain nor sha256 neither sha512 checksum: ${safeStringifyJson(fileInfo)}`, "ERR_UPDATER_NO_CHECKSUM") } return { url: newUrlFromBase(pathTransformer(fileInfo.url), baseUrl), info: fileInfo, } }) const packages = (updateInfo as WindowsUpdateInfo).packages const packageInfo = packages == null ? null : packages[process.arch] || packages.ia32 if (packageInfo != null) { ;(result[0] as any).packageInfo = { ...packageInfo, path: newUrlFromBase(pathTransformer(packageInfo.path), baseUrl).href, } } return result }