Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/builder-util-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export { BlockMap } from "./blockMapApi"
export const CURRENT_APP_INSTALLER_FILE_NAME = "installer.exe"
// nsis-web
export const CURRENT_APP_PACKAGE_FILE_NAME = "package.7z"
// mac zip
export const CURRENT_MAC_APP_ZIP_FILE_NAME = "update.zip"

export function asArray<T>(v: null | undefined | T | Array<T>): Array<T> {
if (v == null) {
Expand Down
62 changes: 62 additions & 0 deletions packages/electron-updater/src/AppUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DownloadOptions,
CancellationError,
ProgressInfo,
BlockMap,
} from "builder-util-runtime"
import { randomBytes } from "crypto"
import { EventEmitter } from "events"
Expand All @@ -29,6 +30,10 @@ import { ProviderPlatform } from "./providers/Provider"
import type { TypedEmitter } from "tiny-typed-emitter"
import Session = Electron.Session
import { AuthInfo } from "electron"
import { gunzipSync } from "zlib"
import { blockmapFiles } from "./util"
import { DifferentialDownloaderOptions } from "./differentialDownloader/DifferentialDownloader"
import { GenericDifferentialDownloader } from "./differentialDownloader/GenericDifferentialDownloader"

export type AppUpdaterEvents = {
error: (error: Error, message?: string) => void
Expand Down Expand Up @@ -689,6 +694,63 @@ export abstract class AppUpdater extends (EventEmitter as new () => TypedEmitter
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) {
// test mode
throw e
}
return true
}
}
}

export interface DownloadUpdateOptions {
Expand Down
13 changes: 9 additions & 4 deletions packages/electron-updater/src/MacUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AllPublishOptions, newError, safeStringifyJson } from "builder-util-runtime"
import { AllPublishOptions, newError, safeStringifyJson, CURRENT_MAC_APP_ZIP_FILE_NAME } from "builder-util-runtime"
import { stat } from "fs-extra"
import { createReadStream } from "fs"
import { createReadStream, copyFileSync } from "fs"
import { createServer, IncomingMessage, Server, ServerResponse } from "http"
import { AppAdapter } from "./AppAdapter"
import { AppUpdater, DownloadUpdateOptions } from "./AppUpdater"
Expand Down Expand Up @@ -79,12 +79,17 @@ export class MacUpdater extends AppUpdater {
throw newError(`ZIP file not provided: ${safeStringifyJson(files)}`, "ERR_UPDATER_ZIP_FILE_NOT_FOUND")
}

const provider = downloadUpdateOptions.updateInfoAndProvider.provider

return this.executeDownload({
fileExtension: "zip",
fileInfo: zipFileInfo,
downloadUpdateOptions,
task: (destinationFile, downloadOptions) => {
return this.httpExecutor.download(zipFileInfo.url, destinationFile, downloadOptions)
task: async (destinationFile, downloadOptions) => {
if (await this.differentialDownloadInstaller(zipFileInfo, downloadUpdateOptions, destinationFile, provider, CURRENT_MAC_APP_ZIP_FILE_NAME)) {
await this.httpExecutor.download(zipFileInfo.url, destinationFile, downloadOptions)
}
copyFileSync(destinationFile, this.downloadedUpdateHelper!.cacheDir + "/update.zip")
},
done: event => this.updateDownloaded(zipFileInfo, event),
})
Expand Down
66 changes: 3 additions & 63 deletions packages/electron-updater/src/NsisUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { AllPublishOptions, newError, PackageFileInfo, BlockMap, CURRENT_APP_PACKAGE_FILE_NAME, CURRENT_APP_INSTALLER_FILE_NAME } from "builder-util-runtime"
import { AllPublishOptions, newError, PackageFileInfo, CURRENT_APP_INSTALLER_FILE_NAME, CURRENT_APP_PACKAGE_FILE_NAME } from "builder-util-runtime"
import * as path from "path"
import { AppAdapter } from "./AppAdapter"
import { DownloadUpdateOptions } from "./AppUpdater"
import { BaseUpdater, InstallOptions } from "./BaseUpdater"
import { DifferentialDownloaderOptions } from "./differentialDownloader/DifferentialDownloader"
import { FileWithEmbeddedBlockMapDifferentialDownloader } from "./differentialDownloader/FileWithEmbeddedBlockMapDifferentialDownloader"
import { GenericDifferentialDownloader } from "./differentialDownloader/GenericDifferentialDownloader"
import { DOWNLOAD_PROGRESS, ResolvedUpdateFileInfo, verifyUpdateCodeSignature } from "./main"
import { blockmapFiles } from "./util"
import { DOWNLOAD_PROGRESS, verifyUpdateCodeSignature } from "./main"
import { findFile, Provider } from "./providers/Provider"
import { unlink } from "fs-extra"
import { verifySignature } from "./windowsExecutableCodeSignatureVerifier"
import { URL } from "url"
import { gunzipSync } from "zlib"

export class NsisUpdater extends BaseUpdater {
/**
Expand Down Expand Up @@ -64,7 +61,7 @@ export class NsisUpdater extends BaseUpdater {
"disableWebInstaller is set to false, you should set it to true if you do not plan on using a web installer. This will default to true in a future version."
)
}
if (isWebInstaller || (await this.differentialDownloadInstaller(fileInfo, downloadUpdateOptions, destinationFile, provider))) {
if (isWebInstaller || (await this.differentialDownloadInstaller(fileInfo, downloadUpdateOptions, destinationFile, provider, CURRENT_APP_INSTALLER_FILE_NAME))) {
await this.httpExecutor.download(fileInfo.url, destinationFile, downloadOptions)
}

Expand Down Expand Up @@ -166,63 +163,6 @@ export class NsisUpdater extends BaseUpdater {
return true
}

private async differentialDownloadInstaller(
fileInfo: ResolvedUpdateFileInfo,
downloadUpdateOptions: DownloadUpdateOptions,
installerPath: string,
provider: Provider<any>
): 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, CURRENT_APP_INSTALLER_FILE_NAME),
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) {
// test mode
throw e
}
return true
}
}

private async differentialDownloadWebPackage(
downloadUpdateOptions: DownloadUpdateOptions,
packageInfo: PackageFileInfo,
Expand Down
19 changes: 11 additions & 8 deletions test/src/updater/differentialUpdateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,25 @@ test.ifAll.ifDevOrLinuxCi("AppImage", () => testLinux(Arch.x64))

test.ifAll.ifDevOrLinuxCi("AppImage ia32", () => testLinux(Arch.ia32))

// ifAll.ifMac.ifNotCi todo
test.skip("dmg", async () => {
async function testMac(arch: Arch) {
process.env.TEST_UPDATER_ARCH = Arch[arch]

const outDirs: Array<string> = []
const tmpDir = new TmpDir("differential-updater-test")
if (process.env.__SKIP_BUILD == null) {
await doBuild(outDirs, Platform.MAC.createTarget(undefined, Arch.x64), tmpDir, {
try {
await doBuild(outDirs, Platform.MAC.createTarget(["dmg"], arch), tmpDir, {
mac: {
electronUpdaterCompatibility: ">=2.17.0",
},
})
} else {
// todo
await testBlockMap(outDirs[0], path.join(outDirs[1]), MacUpdater, "mac/Test App ßW.app", Platform.MAC)
} finally {
await tmpDir.cleanup()
}
}

await testBlockMap(outDirs[0], path.join(outDirs[1]), MacUpdater, "mac/Test App ßW.app", Platform.MAC)
})
test.ifAll.ifMac.ifNotCi("Mac intel", () => testMac(Arch.x64))
test.ifAll.ifMac.ifNotCi("Mac arm64", () => testMac(Arch.arm64))

async function buildApp(version: string, outDirs: Array<string>, targets: Map<Platform, Map<Arch, Array<string>>>, tmpDir: TmpDir, extraConfig: Configuration | null | undefined) {
await assertPack(
Expand Down