import {
AllPublishOptions,
asArray,
CancellationToken,
newError,
PublishConfiguration,
UpdateInfo,
UUID,
DownloadOptions,
CancellationError,
ProgressInfo,
BlockMap,
retry,
} from "builder-util-runtime"
import { randomBytes } from "crypto"
import { release } from "os"
import { EventEmitter } from "events"
import { mkdir, outputFile, readFile, rename, unlink } from "fs-extra"
import { OutgoingHttpHeaders } from "http"
import { load } from "js-yaml"
import { Lazy } from "lazy-val"
import * as path from "path"
import { eq as isVersionsEqual, gt as isVersionGreaterThan, lt as isVersionLessThan, parse as parseVersion, prerelease as getVersionPreleaseComponents, SemVer } from "semver"
import { AppAdapter } from "./AppAdapter"
import { createTempUpdateFile, DownloadedUpdateHelper } from "./DownloadedUpdateHelper"
import { ElectronAppAdapter } from "./ElectronAppAdapter"
import { ElectronHttpExecutor, getNetSession, LoginCallback } from "./electronHttpExecutor"
import { GenericProvider } from "./providers/GenericProvider"
import { createClient, isUrlProbablySupportMultiRangeRequests } from "./providerFactory"
import { Provider, ProviderPlatform } from "./providers/Provider"
import type { TypedEmitter } from "tiny-typed-emitter"
import Session = Electron.Session
import type { AuthInfo } from "electron"
import { gunzipSync } from "zlib"
import { blockmapFiles } from "./util"
import { DifferentialDownloaderOptions } from "./differentialDownloader/DifferentialDownloader"
import { GenericDifferentialDownloader } from "./differentialDownloader/GenericDifferentialDownloader"
import { DOWNLOAD_PROGRESS, Logger, ResolvedUpdateFileInfo, UPDATE_DOWNLOADED, UpdateCheckResult, UpdateDownloadedEvent, UpdaterSignal } from "./types"
import { VerifyUpdateSupport } from "./main"
export type AppUpdaterEvents = {
error: (error: Error, message?: string) => void
login: (info: AuthInfo, callback: LoginCallback) => void
"checking-for-update": () => void
"update-not-available": (info: UpdateInfo) => void
"update-available": (info: UpdateInfo) => void
"update-downloaded": (event: UpdateDownloadedEvent) => void
"download-progress": (info: ProgressInfo) => void
"update-cancelled": (info: UpdateInfo) => void
"appimage-filename-updated": (path: string) => void
}
export abstract class AppUpdater extends (EventEmitter as new () => TypedEmitter<AppUpdaterEvents>) {
* Whether to automatically download an update when it is found.
* @default true
*/
autoDownload = true
* Whether to automatically install a downloaded update on app quit (if `quitAndInstall` was not called before).
* @default true
*/
autoInstallOnAppQuit = true
* Whether to run the app after finish install when run the installer is NOT in silent mode.
* @default true
*/
autoRunAppAfterInstall = true
* *GitHub provider only.* Whether to allow update to pre-release versions. Defaults to `true` if application version contains prerelease components (e.g. `0.12.1-alpha.1`, here `alpha` is a prerelease component), otherwise `false`.
*
* If `true`, downgrade will be allowed (`allowDowngrade` will be set to `true`).
*/
allowPrerelease = false
* *GitHub provider only.* Get all release notes (from current version to latest), not just the latest.
* @default false
*/
fullChangelog = false
* Whether to allow version downgrade (when a user from the beta channel wants to go back to the stable channel).
*
* Taken in account only if channel differs (pre-release version component in terms of semantic versioning).
*
* @default false
*/
allowDowngrade = false
* Web installer files might not have signature verification, this switch prevents to load them unless it is needed.
*
* Currently false to prevent breaking the current API, but it should be changed to default true at some point that
* breaking changes are allowed.
*
* @default false
*/
disableWebInstaller = false
* *NSIS only* Disable differential downloads and always perform full download of installer.
*
* @default false
*/
disableDifferentialDownload = false
* Allows developer to force the updater to work in "dev" mode, looking for "dev-app-update.yml" instead of "app-update.yml"
* Dev: `path.join(this.app.getAppPath(), "dev-app-update.yml")`
* Prod: `path.join(process.resourcesPath!, "app-update.yml")`
*
* @default false
*/
forceDevUpdateConfig = false
* The current application version.
*/
readonly currentVersion: SemVer
private _channel: string | null = null
protected downloadedUpdateHelper: DownloadedUpdateHelper | null = null
* Get the update channel. Doesn't return `channel` from the update configuration, only if was previously set.
*/
get channel(): string | null {
return this._channel
}
* Set the update channel. Overrides `channel` in the update configuration.
*
* `allowDowngrade` will be automatically set to `true`. If this behavior is not suitable for you, simple set `allowDowngrade` explicitly after.
*/
set channel(value: string | null) {
if (this._channel != null) {
if (typeof value !== "string") {
throw newError(`Channel must be a string, but got: ${value}`, "ERR_UPDATER_INVALID_CHANNEL")
} else if (value.length === 0) {
throw newError(`Channel must be not an empty string`, "ERR_UPDATER_INVALID_CHANNEL")
}
}
this._channel = value
this.allowDowngrade = true
}
* The request headers.
*/
requestHeaders: OutgoingHttpHeaders | null = null
* Shortcut for explicitly adding auth tokens to request headers
*/
addAuthHeader(token: string) {
this.requestHeaders = Object.assign({}, this.requestHeaders, {
authorization: token,
})
}
protected _logger: Logger = console
get netSession(): Session {
return getNetSession()
}
* The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`.
* Set it to `null` if you would like to disable a logging feature.
*/
get logger(): Logger | null {
return this._logger
}
set logger(value: Logger | null) {
this._logger = value == null ? new NoOpLogger() : value
}
* For type safety you can use signals, e.g. `autoUpdater.signals.updateDownloaded(() => {})` instead of `autoUpdater.on('update-available', () => {})`
*/
readonly signals = new UpdaterSignal(this)
private _appUpdateConfigPath: string | null = null
* test only
* @private
*/
set updateConfigPath(value: string | null) {
this.clientPromise = null
this._appUpdateConfigPath = value
this.configOnDisk = new Lazy<any>(() => this.loadUpdateConfig())
}
protected _isUpdateSupported: VerifyUpdateSupport = updateInfo => this.checkIfUpdateSupported(updateInfo)
* Allows developer to override default logic for determining if an update is supported.
* The default logic compares the `UpdateInfo` minimum system version against the `os.release()` with `semver` package
*/
get isUpdateSupported(): VerifyUpdateSupport {
return this._isUpdateSupported
}
set isUpdateSupported(value: VerifyUpdateSupport) {
if (value) {
this._isUpdateSupported = value
}
}
protected _isUserWithinRollout: VerifyUpdateSupport = updateInfo => this.isStagingMatch(updateInfo)
* Allows developer to override default logic for determining if the user is below the rollout threshold.
* The default logic compares the staging percentage with numerical representation of user ID.
* An override can define custom logic, or bypass it if needed.
*/
get isUserWithinRollout(): VerifyUpdateSupport {
return this._isUserWithinRollout
}
set isUserWithinRollout(value: VerifyUpdateSupport) {
if (value) {
this._isUserWithinRollout = value
}
}
private clientPromise: Promise<Provider<any>> | null = null
protected readonly stagingUserIdPromise = new Lazy<string>(() => this.getOrCreateStagingUserId())
configOnDisk = new Lazy<any>(() => this.loadUpdateConfig())
private checkForUpdatesPromise: Promise<UpdateCheckResult> | null = null
private downloadPromise: Promise<Array<string>> | null = null
protected readonly app: AppAdapter
protected updateInfoAndProvider: UpdateInfoAndProvider | null = null
readonly httpExecutor: ElectronHttpExecutor
protected constructor(options: AllPublishOptions | null | undefined, app?: AppAdapter) {
super()
this.on("error", (error: Error) => {
this._logger.error(`Error: ${error.stack || error.message}`)
})
if (app == null) {
this.app = new ElectronAppAdapter()
this.httpExecutor = new ElectronHttpExecutor((authInfo, callback) => this.emit("login", authInfo, callback))
} else {
this.app = app
this.httpExecutor = null as any
}
const currentVersionString = this.app.version
const currentVersion = parseVersion(currentVersionString)
if (currentVersion == null) {
throw newError(`App version is not a valid semver version: "${currentVersionString}"`, "ERR_UPDATER_INVALID_VERSION")
}
this.currentVersion = currentVersion
this.allowPrerelease = hasPrereleaseComponents(currentVersion)
if (options != null) {
this.setFeedURL(options)
if (typeof options !== "string" && options.requestHeaders) {
this.requestHeaders = options.requestHeaders
}
}
}
getFeedURL(): string | null | undefined {
return "Deprecated. Do not use it."
}
* Configure update provider. If value is `string`, [GenericServerOptions](./publish.md#genericserveroptions) will be set with value as `url`.
* @param options If you want to override configuration in the `app-update.yml`.
*/
setFeedURL(options: PublishConfiguration | AllPublishOptions | string) {
const runtimeOptions = this.createProviderRuntimeOptions()
let provider: Provider<any>
if (typeof options === "string") {
provider = new GenericProvider({ provider: "generic", url: options }, this, {
...runtimeOptions,
isUseMultipleRangeRequest: isUrlProbablySupportMultiRangeRequests(options),
})
} else {
provider = createClient(options, this, runtimeOptions)
}
this.clientPromise = Promise.resolve(provider)
}
* Asks the server whether there is an update.
* @returns null if the updater is disabled, otherwise info about the latest version
*/
checkForUpdates(): Promise<UpdateCheckResult | null> {
if (!this.isUpdaterActive()) {
return Promise.resolve(null)
}
let checkForUpdatesPromise = this.checkForUpdatesPromise
if (checkForUpdatesPromise != null) {
this._logger.info("Checking for update (already in progress)")
return checkForUpdatesPromise
}
const nullizePromise = () => (this.checkForUpdatesPromise = null)
this._logger.info("Checking for update")
checkForUpdatesPromise = this.doCheckForUpdates()
.then(it => {
nullizePromise()
return it
})
.catch((e: any) => {
nullizePromise()
this.emit("error", e, `Cannot check for updates: ${(e.stack || e).toString()}`)
throw e
})
this.checkForUpdatesPromise = checkForUpdatesPromise
return checkForUpdatesPromise
}
public isUpdaterActive(): boolean {
const isEnabled = this.app.isPackaged || this.forceDevUpdateConfig
if (!isEnabled) {
this._logger.info("Skip checkForUpdates because application is not packed and dev update config is not forced")
return false
}
return true
}
checkForUpdatesAndNotify(downloadNotification?: DownloadNotification): Promise<UpdateCheckResult | null> {
return this.checkForUpdates().then(it => {
if (!it?.downloadPromise) {
if (this._logger.debug != null) {
this._logger.debug("checkForUpdatesAndNotify called, downloadPromise is null")
}
return it
}
void it.downloadPromise.then(() => {
const notificationContent = AppUpdater.formatDownloadNotification(it.updateInfo.version, this.app.name, downloadNotification)
new (require("electron").Notification)(notificationContent).show()
})
return it
})
}
private static formatDownloadNotification(version: string, appName: string, downloadNotification?: DownloadNotification): DownloadNotification {
if (downloadNotification == null) {
downloadNotification = {
title: "A new update is ready to install",
body: `{appName} version {version} has been downloaded and will be automatically installed on exit`,
}
}
downloadNotification = {
title: downloadNotification.title.replace("{appName}", appName).replace("{version}", version),
body: downloadNotification.body.replace("{appName}", appName).replace("{version}", version),
}
return downloadNotification
}
private async isStagingMatch(updateInfo: UpdateInfo): Promise<boolean> {
const rawStagingPercentage = updateInfo.stagingPercentage
let stagingPercentage = rawStagingPercentage
if (stagingPercentage == null) {
return true
}
stagingPercentage = parseInt(stagingPercentage as any, 10)
if (isNaN(stagingPercentage)) {
this._logger.warn(`Staging percentage is NaN: ${rawStagingPercentage}`)
return true
}
stagingPercentage = stagingPercentage / 100
const stagingUserId = await this.stagingUserIdPromise.value
const val = UUID.parse(stagingUserId).readUInt32BE(12)
const percentage = val / 0xffffffff
this._logger.info(`Staging percentage: ${stagingPercentage}, percentage: ${percentage}, user id: ${stagingUserId}`)
return percentage < stagingPercentage
}
private computeFinalHeaders(headers: OutgoingHttpHeaders) {
if (this.requestHeaders != null) {
Object.assign(headers, this.requestHeaders)
}
return headers
}
private async isUpdateAvailable(updateInfo: UpdateInfo): Promise<boolean> {
const latestVersion = parseVersion(updateInfo.version)
if (latestVersion == null) {
throw newError(
`This file could not be downloaded, or the latest version (from update server) does not have a valid semver version: "${updateInfo.version}"`,
"ERR_UPDATER_INVALID_VERSION"
)
}
const currentVersion = this.currentVersion
if (isVersionsEqual(latestVersion, currentVersion)) {
return false
}
if (!(await Promise.resolve(this.isUpdateSupported(updateInfo)))) {
return false
}
const isUserWithinRollout = await Promise.resolve(this.isUserWithinRollout(updateInfo))
if (!isUserWithinRollout) {
return false
}
const isLatestVersionNewer = isVersionGreaterThan(latestVersion, currentVersion)
const isLatestVersionOlder = isVersionLessThan(latestVersion, currentVersion)
if (isLatestVersionNewer) {
return true
}
return this.allowDowngrade && isLatestVersionOlder
}
private checkIfUpdateSupported(updateInfo: UpdateInfo) {
const minimumSystemVersion = updateInfo?.minimumSystemVersion
const currentOSVersion = release()
if (minimumSystemVersion) {
try {
if (isVersionLessThan(currentOSVersion, minimumSystemVersion)) {
this._logger.info(`Current OS version ${currentOSVersion} is less than the minimum OS version required ${minimumSystemVersion} for version ${currentOSVersion}`)
return false
}
} catch (e: any) {
this._logger.warn(`Failed to compare current OS version(${currentOSVersion}) with minimum OS version(${minimumSystemVersion}): ${(e.message || e).toString()}`)
}
}
return true
}
protected async getUpdateInfoAndProvider(): Promise<UpdateInfoAndProvider> {
await this.app.whenReady()
if (this.clientPromise == null) {
this.clientPromise = this.configOnDisk.value.then(it => createClient(it, this, this.createProviderRuntimeOptions()))
}
const client = await this.clientPromise
const stagingUserId = await this.stagingUserIdPromise.value
client.setRequestHeaders(this.computeFinalHeaders({ "x-user-staging-id": stagingUserId }))
return {
info: await client.getLatestVersion(),
provider: client,
}
}
private createProviderRuntimeOptions() {
return {
isUseMultipleRangeRequest: true,
platform: this._testOnlyOptions == null ? (process.platform as ProviderPlatform) : this._testOnlyOptions.platform,
executor: this.httpExecutor,
}
}
private async doCheckForUpdates(): Promise<UpdateCheckResult> {
this.emit("checking-for-update")
const result = await this.getUpdateInfoAndProvider()
const updateInfo = result.info
if (!(await this.isUpdateAvailable(updateInfo))) {
this._logger.info(
`Update for version ${this.currentVersion.format()} is not available (latest version: ${updateInfo.version}, downgrade is ${
this.allowDowngrade ? "allowed" : "disallowed"
}).`
)
this.emit("update-not-available", updateInfo)
return {
isUpdateAvailable: false,
versionInfo: updateInfo,
updateInfo,
}
}
this.updateInfoAndProvider = result
this.onUpdateAvailable(updateInfo)
const cancellationToken = new CancellationToken()
return {
isUpdateAvailable: true,
versionInfo: updateInfo,
updateInfo,
cancellationToken,
downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null,
}
}
protected onUpdateAvailable(updateInfo: UpdateInfo): void {
this._logger.info(
`Found version ${updateInfo.version} (url: ${asArray(updateInfo.files)
.map(it => it.url)
.join(", ")})`
)
this.emit("update-available", updateInfo)
}
* Start downloading update manually. You can use this method if `autoDownload` option is set to `false`.
* @returns {Promise<Array<string>>} Paths to downloaded files.
*/
downloadUpdate(cancellationToken: CancellationToken = new CancellationToken()): Promise<Array<string>> {
const updateInfoAndProvider = this.updateInfoAndProvider
if (updateInfoAndProvider == null) {
const error = new Error("Please check update first")
this.dispatchError(error)
return Promise.reject(error)
}
if (this.downloadPromise != null) {
this._logger.info("Downloading update (already in progress)")
return this.downloadPromise
}
this._logger.info(
`Downloading update from ${asArray(updateInfoAndProvider.info.files)
.map(it => it.url)
.join(", ")}`
)
const errorHandler = (e: Error): Error => {
if (!(e instanceof CancellationError)) {
try {
this.dispatchError(e)
} catch (nestedError: any) {
this._logger.warn(`Cannot dispatch error event: ${nestedError.stack || nestedError}`)
}
}
return e
}
this.downloadPromise = this.doDownloadUpdate({
updateInfoAndProvider,
requestHeaders: this.computeRequestHeaders(updateInfoAndProvider.provider),
cancellationToken,
disableWebInstaller: this.disableWebInstaller,
disableDifferentialDownload: this.disableDifferentialDownload,
})
.catch((e: any) => {
throw errorHandler(e)
})
.finally(() => {
this.downloadPromise = null
})
return this.downloadPromise
}
protected dispatchError(e: Error): void {
this.emit("error", e, (e.stack || e).toString())
}
protected dispatchUpdateDownloaded(event: UpdateDownloadedEvent): void {
this.emit(UPDATE_DOWNLOADED, event)
}
protected abstract doDownloadUpdate(downloadUpdateOptions: DownloadUpdateOptions): Promise<Array<string>>
* Restarts the app and installs the update after it has been downloaded.
* It should only be called after `update-downloaded` has been emitted.
*
* **Note:** `autoUpdater.quitAndInstall()` will close all application windows first and only emit `before-quit` event on `app` after that.
* This is different from the normal quit event sequence.
*
* @param isSilent *windows-only* Runs the installer in silent mode. Defaults to `false`.
* @param isForceRunAfter Run the app after finish even on silent install. Not applicable for macOS.
* Ignored if `isSilent` is set to `false`(In this case you can still set `autoRunAppAfterInstall` to `false` to prevent run the app after finish).
*/
abstract quitAndInstall(isSilent?: boolean, isForceRunAfter?: boolean): void
private async loadUpdateConfig(): Promise<any> {
if (this._appUpdateConfigPath == null) {
this._appUpdateConfigPath = this.app.appUpdateConfigPath
}
return load(await readFile(this._appUpdateConfigPath, "utf-8"))
}
private computeRequestHeaders(provider: Provider<any>): OutgoingHttpHeaders {
const fileExtraDownloadHeaders = provider.fileExtraDownloadHeaders
if (fileExtraDownloadHeaders != null) {
const requestHeaders = this.requestHeaders
return requestHeaders == null
? fileExtraDownloadHeaders
: {
...fileExtraDownloadHeaders,
...requestHeaders,
}
}
return this.computeFinalHeaders({ accept: "*/*" })
}
private async getOrCreateStagingUserId(): Promise<string> {
const file = path.join(this.app.userDataPath, ".updaterId")
try {
const id = await readFile(file, "utf-8")
if (UUID.check(id)) {
return id
} else {
this._logger.warn(`Staging user id file exists, but content was invalid: ${id}`)
}
} catch (e: any) {
if (e.code !== "ENOENT") {
this._logger.warn(`Couldn't read staging user ID, creating a blank one: ${e}`)
}
}
const id = UUID.v5(randomBytes(4096), UUID.OID)
this._logger.info(`Generated new staging user ID: ${id}`)
try {
await outputFile(file, id)
} catch (e: any) {
this._logger.warn(`Couldn't write out staging user ID: ${e}`)
}
return id
}
get isAddNoCacheQuery(): boolean {
const headers = this.requestHeaders
if (headers == null) {
return true
}
for (const headerName of Object.keys(headers)) {
const s = headerName.toLowerCase()
if (s === "authorization" || s === "private-token") {
return false
}
}
return true
}
* @private
* @internal
*/
_testOnlyOptions: TestOnlyUpdaterOptions | null = null
private async getOrCreateDownloadHelper(): Promise<DownloadedUpdateHelper> {
let result = this.downloadedUpdateHelper
if (result == null) {
const dirName = (await this.configOnDisk.value).updaterCacheDirName
const logger = this._logger
if (dirName == null) {
logger.error("updaterCacheDirName is not specified in app-update.yml Was app build using at least electron-builder 20.34.0?")
}
const cacheDir = path.join(this.app.baseCachePath, dirName || this.app.name)
if (logger.debug != null) {
logger.debug(`updater cache dir: ${cacheDir}`)
}
result = new DownloadedUpdateHelper(cacheDir)
this.downloadedUpdateHelper = result
}
return result
}
protected async executeDownload(taskOptions: DownloadExecutorTask): Promise<Array<string>> {
const fileInfo = taskOptions.fileInfo
const downloadOptions: DownloadOptions = {
headers: taskOptions.downloadUpdateOptions.requestHeaders,
cancellationToken: taskOptions.downloadUpdateOptions.cancellationToken,
sha2: (fileInfo.info as any).sha2,
sha512: fileInfo.info.sha512,
}
if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) {
downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it)
}
const updateInfo = taskOptions.downloadUpdateOptions.updateInfoAndProvider.info
const version = updateInfo.version
const packageInfo = fileInfo.packageInfo
function getCacheUpdateFileName(): string {
const urlPath = decodeURIComponent(taskOptions.fileInfo.url.pathname)
if (urlPath.toLowerCase().endsWith(`.${taskOptions.fileExtension.toLowerCase()}`)) {
return path.basename(urlPath)
} else {
return taskOptions.fileInfo.info.url
}
}
const downloadedUpdateHelper = await this.getOrCreateDownloadHelper()
const cacheDir = downloadedUpdateHelper.cacheDirForPendingUpdate
await mkdir(cacheDir, { recursive: true })
const updateFileName = getCacheUpdateFileName()
let updateFile = path.join(cacheDir, updateFileName)
const packageFile = packageInfo == null ? null : path.join(cacheDir, `package-${version}${path.extname(packageInfo.path) || ".7z"}`)
const done = async (isSaveCache: boolean) => {
await downloadedUpdateHelper.setDownloadedFile(updateFile, packageFile, updateInfo, fileInfo, updateFileName, isSaveCache)
await taskOptions.done!({
...updateInfo,
downloadedFile: updateFile,
})
return packageFile == null ? [updateFile] : [updateFile, packageFile]
}
const log = this._logger
const cachedUpdateFile = await downloadedUpdateHelper.validateDownloadedPath(updateFile, updateInfo, fileInfo, log)
if (cachedUpdateFile != null) {
updateFile = cachedUpdateFile
return await done(false)
}
const removeFileIfAny = async () => {
await downloadedUpdateHelper.clear().catch(() => {
})
return await unlink(updateFile).catch(() => {
})
}
const tempUpdateFile = await createTempUpdateFile(`temp-${updateFileName}`, cacheDir, log)
try {
await taskOptions.task(tempUpdateFile, downloadOptions, packageFile, removeFileIfAny)
await retry(
() => rename(tempUpdateFile, updateFile),
60,
500,
0,
0,
error => error instanceof Error && /^EBUSY:/.test(error.message)
)
} catch (e: any) {
await removeFileIfAny()
if (e instanceof CancellationError) {
log.info("cancelled")
this.emit("update-cancelled", updateInfo)
}
throw e
}
log.info(`New version ${version} has been downloaded to ${updateFile}`)
return await done(true)
}
protected async differentialDownloadInstaller(
fileInfo: ResolvedUpdateFileInfo,
downloadUpdateOptions: DownloadUpdateOptions,
installerPath: string,
provider: Provider<any>,
oldInstallerFileName: string
): Promise<boolean> {
try {
if (this._testOnlyOptions != null && !this._testOnlyOptions.isUseDifferentialDownload) {
return true
}
const blockmapFileUrls = blockmapFiles(fileInfo.url, this.app.version, downloadUpdateOptions.updateInfoAndProvider.info.version)
this._logger.info(`Download block maps (old: "${blockmapFileUrls[0]}", new: ${blockmapFileUrls[1]})`)
const downloadBlockMap = async (url: URL): Promise<BlockMap> => {
const data = await this.httpExecutor.downloadToBuffer(url, {
headers: downloadUpdateOptions.requestHeaders,
cancellationToken: downloadUpdateOptions.cancellationToken,
})
if (data == null || data.length === 0) {
throw new Error(`Blockmap "${url.href}" is empty`)
}
try {
return JSON.parse(gunzipSync(data).toString())
} catch (e: any) {
throw new Error(`Cannot parse blockmap "${url.href}", error: ${e}`)
}
}
const downloadOptions: DifferentialDownloaderOptions = {
newUrl: fileInfo.url,
oldFile: path.join(this.downloadedUpdateHelper!.cacheDir, oldInstallerFileName),
logger: this._logger,
newFile: installerPath,
isUseMultipleRangeRequest: provider.isUseMultipleRangeRequest,
requestHeaders: downloadUpdateOptions.requestHeaders,
cancellationToken: downloadUpdateOptions.cancellationToken,
}
if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) {
downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it)
}
const blockMapDataList = await Promise.all(blockmapFileUrls.map(u => downloadBlockMap(u)))
await new GenericDifferentialDownloader(fileInfo.info, this.httpExecutor, downloadOptions).download(blockMapDataList[0], blockMapDataList[1])
return false
} catch (e: any) {
this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`)
if (this._testOnlyOptions != null) {
throw e
}
return true
}
}
}
export interface DownloadUpdateOptions {
readonly updateInfoAndProvider: UpdateInfoAndProvider
readonly requestHeaders: OutgoingHttpHeaders
readonly cancellationToken: CancellationToken
readonly disableWebInstaller?: boolean
readonly disableDifferentialDownload?: boolean
}
function hasPrereleaseComponents(version: SemVer) {
const versionPrereleaseComponent = getVersionPreleaseComponents(version)
return versionPrereleaseComponent != null && versionPrereleaseComponent.length > 0
}
export class NoOpLogger implements Logger {
info(message?: any) {
}
warn(message?: any) {
}
error(message?: any) {
}
}
export interface UpdateInfoAndProvider {
info: UpdateInfo
provider: Provider<any>
}
export interface DownloadExecutorTask {
readonly fileExtension: string
readonly fileInfo: ResolvedUpdateFileInfo
readonly downloadUpdateOptions: DownloadUpdateOptions
readonly task: (destinationFile: string, downloadOptions: DownloadOptions, packageFile: string | null, removeTempDirIfAny: () => Promise<any>) => Promise<any>
readonly done?: (event: UpdateDownloadedEvent) => Promise<any>
}
export interface DownloadNotification {
body: string
title: string
}
export interface TestOnlyUpdaterOptions {
platform: ProviderPlatform
isUseDifferentialDownload?: boolean
}