From 14ff2764c3e614a3c0a57b590731035b0c5be318 Mon Sep 17 00:00:00 2001 From: develar Date: Fri, 9 Sep 2016 10:11:14 +0200 Subject: [PATCH] =?UTF-8?q?WIP:=20NSIS=20Auto-Update=20#529=20=E2=80=94=20?= =?UTF-8?q?file=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- nsis-auto-updater/package.json | 11 ++- nsis-auto-updater/src/api.ts | 18 +++++ nsis-auto-updater/src/nsis-updater.ts | 112 +++++++++++++++++--------- nsis-auto-updater/tsconfig.json | 4 + package.json | 6 +- src/builder.ts | 8 +- src/publish/BintrayPublisher.ts | 12 +-- src/publish/bintray.ts | 25 +++++- test/src/ArtifactPublisherTest.ts | 2 +- test/src/helpers/fileAssert.ts | 4 + test/src/nsisUpdaterTest.ts | 28 ++++++- 12 files changed, 170 insertions(+), 63 deletions(-) create mode 100644 nsis-auto-updater/src/api.ts diff --git a/.gitignore b/.gitignore index d19f9ccf57a..bb560693a09 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ dist/ /typings/main.d.ts .DS_Store .idea/shelf/ -test/typings/electron-builder.d.ts \ No newline at end of file +/test/typings/electron-builder.d.ts +/test/typings/nsis-updater.d.ts \ No newline at end of file diff --git a/nsis-auto-updater/package.json b/nsis-auto-updater/package.json index b2907c476be..113431dcb3a 100644 --- a/nsis-auto-updater/package.json +++ b/nsis-auto-updater/package.json @@ -12,11 +12,14 @@ "out" ], "dependencies": { - "bluebird": "^3.4.1", - "fs-extra-p": "^1.0.6" + "bluebird": "^3.4.6", + "fs-extra-p": "^1.1.8", + "semver": "^5.3.0" }, "bundledDependencies": [ "fs-extra-p", - "bluebird" - ] + "bluebird", + "semver" + ], + "typings": "./out/nsis-updater.d.ts" } diff --git a/nsis-auto-updater/src/api.ts b/nsis-auto-updater/src/api.ts new file mode 100644 index 00000000000..56032a1a270 --- /dev/null +++ b/nsis-auto-updater/src/api.ts @@ -0,0 +1,18 @@ +export interface VersionInfo { + readonly version: string +} +export interface FileInfo { + url: string +} + +export interface Provider { + getLatestVersion(): Promise + + getUpdateFile(versionInfo: VersionInfo): Promise +} + +export interface UpdateCheckResult { + readonly versionInfo: VersionInfo + + readonly downloadPromise?: Promise | null +} \ No newline at end of file diff --git a/nsis-auto-updater/src/nsis-updater.ts b/nsis-auto-updater/src/nsis-updater.ts index 6cf1275aa25..4280cc3b0bb 100644 --- a/nsis-auto-updater/src/nsis-updater.ts +++ b/nsis-auto-updater/src/nsis-updater.ts @@ -2,40 +2,23 @@ import { EventEmitter } from "events" import { spawn } from "child_process" import * as path from "path" import { tmpdir } from "os" -import { Promise as BluebirdPromise } from "bluebird" -import { BintrayClient } from "../../src/publish/bintray" +import { BintrayClient, BintrayOptions } from "../../src/publish/bintray" import { HttpError } from "../../src/publish/restApiRequest" +import semver = require("semver") +import { download } from "../../src/util/httpRequest" +import { Provider, VersionInfo, UpdateCheckResult, FileInfo } from "./api" //noinspection JSUnusedLocalSymbols const __awaiter = require("../../src/util/awaiter") -interface VersionInfo { - version: string -} - -interface Provider { - checkForUpdates(): Promise -} - -//noinspection ReservedWordAsName -interface BintraySourceMetadata { - // e.g. develar - readonly user: string - // e.g. onshape-desktop-shell - readonly package: string - - // e.g. generic or bin, defaults to generic - readonly repository?: string | null -} - -class BintrayProvider { +class BintrayProvider implements Provider { private client: BintrayClient - constructor(configuration: BintraySourceMetadata) { - this.client = new BintrayClient(configuration.user, configuration.package, configuration.repository || "generic") + constructor(configuration: BintrayOptions) { + this.client = new BintrayClient(configuration.user, configuration.package, configuration.repo) } - async checkForUpdates(): Promise { + async getLatestVersion(): Promise { try { const data = await this.client.getVersion("_latest") return { @@ -44,14 +27,37 @@ class BintrayProvider { } catch (e) { if (e instanceof HttpError && e.response.statusCode === 404) { - throw new Error(`No latest version, please ensure that user, repository and package correctly configured. Or at least one version is published.${e.stack || e.message}`) + throw new Error(`No latest version, please ensure that user, package and repository correctly configured. Or at least one version is published. ${e.stack || e.message}`) + } + throw e + } + } + + async getUpdateFile(versionInfo: VersionInfo): Promise { + try { + const files = await this.client.getVersionFiles(versionInfo.version) + const suffix = `${versionInfo.version}.exe` + for (let file of files) { + if (file.name.endsWith(suffix) && file.name.includes("Setup")) { + return { + url: "" + } + } + } + + //noinspection ExceptionCaughtLocallyJS + throw new Error(`Cannot find suitable file for version ${versionInfo.version} in: ${JSON.stringify(files, null, 2)}`) + } + catch (e) { + if (e instanceof HttpError && e.response.statusCode === 404) { + throw new Error(`No latest version, please ensure that user, package and repository correctly configured. Or at least one version is published. ${e.stack || e.message}`) } throw e } } } -class NsisUpdater extends EventEmitter { +export class NsisUpdater extends EventEmitter { private setupPath = path.join(tmpdir(), 'innobox-upgrade.exe') private updateAvailable = false @@ -59,29 +65,64 @@ class NsisUpdater extends EventEmitter { private client: Provider + private readonly app: any + constructor(public updateUrl?: string) { super() + + this.app = (global).__test_app || require("electron").app } getFeedURL(): string | null | undefined { return this.updateUrl } - setFeedURL(value: string | BintraySourceMetadata) { + setFeedURL(value: string | BintrayOptions) { this.updateUrl = value.toString() - this.client = new BintrayProvider(value) + this.client = new BintrayProvider(value) } - checkForUpdates(): Promise { + async checkForUpdates(): Promise { if (this.updateUrl == null) { const message = "Update URL is not set" this.emitError(message) - return BluebirdPromise.reject(new Error(message)) + throw new Error(message) } this.emit("checking-for-update") - return this.client.checkForUpdates() + const versionInfo = await this.client.getLatestVersion() + + const latestVersion = semver.valid(versionInfo.version) + if (latestVersion == null) { + const error = `Latest version (from update server) is not valid semver version: "${latestVersion}` + this.emitError(error) + throw new Error(error) + } + + const currentVersion = semver.valid(this.app.getVersion()) + if (currentVersion == null) { + const error = `App version is not valid semver version: "${currentVersion}` + this.emitError(error) + throw new Error(error) + } + + if (semver.gte(currentVersion, latestVersion)) { + this.updateAvailable = false + this.emit("update-not-available") + return { + versionInfo: versionInfo, + } + } + + this.updateAvailable = true + this.emit("update-available") + + return { + versionInfo: versionInfo, + downloadPromise: this.client.getUpdateFile(versionInfo) + .then(it => {}), + } } quitAndInstall(): void { @@ -102,14 +143,11 @@ class NsisUpdater extends EventEmitter { stdio: "ignore", }).unref() - require("electron").app.quit() + this.app.quit() } // emit both error object and message, this is to keep compatibility with old APIs private emitError (message: string) { return this.emit("error", new Error(message), message) } -} - -const updater = new NsisUpdater() -export= updater \ No newline at end of file +} \ No newline at end of file diff --git a/nsis-auto-updater/tsconfig.json b/nsis-auto-updater/tsconfig.json index 6597298bdbb..cd7c5b997cf 100755 --- a/nsis-auto-updater/tsconfig.json +++ b/nsis-auto-updater/tsconfig.json @@ -15,8 +15,12 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true }, + "declaration": { + "": "../test/typings/nsis-updater.d.ts" + }, "files": [ "../node_modules/@types/node/index.d.ts", + "../node_modules/@types/semver/index.d.ts", "../node_modules/fs-extra-p/index.d.ts", "../node_modules/fs-extra-p/bluebird.d.ts", "../src/util/httpRequest.ts", diff --git a/package.json b/package.json index a5327de9bfb..23175c3b3d6 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "node-gyp-rebuild": "./out/node-gyp-rebuild.js" }, "scripts": { - "compile": "npm run compile-production && npm run compile-test && npm run compile-updater", + "compile": "npm run compile-production && npm run compile-updater && npm run compile-test", "compile-production": "ts-babel", "compile-test": "ts-babel test", - "compile-updater": "tsc -p nsis-auto-updater", + "compile-updater": "ts-babel nsis-auto-updater", "lint": "tslint 'src/**/*.ts' 'test/src/**/*.ts'", "pretest": "npm run compile && npm run lint", "test": "node ./test/out/helpers/runTests.js", @@ -121,7 +121,7 @@ "json8": "^0.9.2", "path-sort": "^0.1.0", "pre-git": "^3.10.0", - "ts-babel": "^1.0.4", + "ts-babel": "^1.0.6", "tslint": "^3.15.1", "typescript": "^2.0.2", "whitespace": "^2.1.0" diff --git a/src/builder.ts b/src/builder.ts index e71dcf0c9c1..81f95c1f4c1 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -9,7 +9,8 @@ import { log, warn } from "./util/log" import { Platform, Arch, archFromString } from "./metadata" import { getRepositoryInfo } from "./repositoryInfo" import { DIR_TARGET } from "./targets/targetFactory" -import { BintrayPublisher, BintrayConfiguration } from "./publish/BintrayPublisher" +import { BintrayPublisher } from "./publish/BintrayPublisher" +import { BintrayOptions } from "./publish/bintray" //noinspection JSUnusedLocalSymbols const __awaiter = require("./util/awaiter") @@ -309,8 +310,9 @@ export async function createPublisher(packager: Packager, options: PublishOption } if (publisherName === "bintray") { const version = packager.metadata.version! - const bintrayInfo: BintrayConfiguration = {user: info.user, packageName: info.project, repo: "generic"} - log(`Creating Bintray Publisher — user: ${bintrayInfo.user}, package: ${bintrayInfo.packageName}, repository: ${bintrayInfo.repo}, version: ${version}`) + //noinspection ReservedWordAsName + const bintrayInfo: BintrayOptions = {user: info.user, package: info.project, repo: "generic"} + log(`Creating Bintray Publisher — user: ${bintrayInfo.user}, package: ${bintrayInfo.package}, repository: ${bintrayInfo.repo}, version: ${version}`) return new BintrayPublisher(bintrayInfo, version, options) } return null diff --git a/src/publish/BintrayPublisher.ts b/src/publish/BintrayPublisher.ts index 6e657d5ef53..32b370fe5e1 100644 --- a/src/publish/BintrayPublisher.ts +++ b/src/publish/BintrayPublisher.ts @@ -6,24 +6,18 @@ import { log } from "../util/log" import { debug } from "../util/util" import { basename } from "path" import { stat } from "fs-extra-p" -import { BintrayClient, Version } from "./bintray" +import { BintrayClient, Version, BintrayOptions } from "./bintray" //noinspection JSUnusedLocalSymbols const __awaiter = require("../util/awaiter") -export interface BintrayConfiguration { - readonly user: string - readonly packageName: string - readonly repo?: string -} - export class BintrayPublisher implements Publisher { private _versionPromise: BluebirdPromise private readonly client: BintrayClient - constructor(private info: BintrayConfiguration, private version: string, private options: PublishOptions) { - this.client = new BintrayClient(info.user, info.packageName, info.repo || "generic", options.bintrayToken) + constructor(private info: BintrayOptions, private version: string, private options: PublishOptions) { + this.client = new BintrayClient(info.user, info.package, info.repo, options.bintrayToken) this._versionPromise = >this.init() } diff --git a/src/publish/bintray.ts b/src/publish/bintray.ts index a529ea0d026..df3c4575a0f 100644 --- a/src/publish/bintray.ts +++ b/src/publish/bintray.ts @@ -1,16 +1,33 @@ import { bintrayRequest } from "./restApiRequest" -//noinspection ReservedWordAsName export interface Version { readonly name: string + //noinspection ReservedWordAsName readonly package: string } +export interface File { + name: string + path: string + + sha1: string + sha256: string +} + +export interface BintrayOptions { + readonly user: string + //noinspection ReservedWordAsName + readonly package: string + readonly repo?: string +} + export class BintrayClient { private readonly basePath: string readonly auth: string | null + readonly repo: string - constructor(public user: string, public packageName: string, public repo: string = "generic", apiKey?: string | null) { + constructor(public user: string, public packageName: string, repo?: string, apiKey?: string | null) { + this.repo = repo || "generic" this.auth = apiKey == null ? null : `Basic ${new Buffer(`${user}:${apiKey}`).toString("base64")}` this.basePath = `/packages/${this.user}/${this.repo}/${this.packageName}` } @@ -19,6 +36,10 @@ export class BintrayClient { return bintrayRequest(`${this.basePath}/versions/${version}`, this.auth) } + getVersionFiles(version: string): Promise> { + return bintrayRequest>(`${this.basePath}/versions/${version}/files`, this.auth) + } + createVersion(version: string): Promise { return bintrayRequest(`${this.basePath}/versions`, this.auth, { name: version, diff --git a/test/src/ArtifactPublisherTest.ts b/test/src/ArtifactPublisherTest.ts index 1c3b94d87b1..a70398a6784 100644 --- a/test/src/ArtifactPublisherTest.ts +++ b/test/src/ArtifactPublisherTest.ts @@ -54,7 +54,7 @@ function testAndIgnoreApiRate(name: string, testFunction: () => Promise) { test("Bintray upload", async () => { const version = versionNumber() //noinspection SpellCheckingInspection - const publisher = new BintrayPublisher({user: "actperepo", packageName: "test", repo: "generic"}, version, {bintrayToken: "5df2cadec86dff91392e4c419540785813c3db15"}) + const publisher = new BintrayPublisher({user: "actperepo", package: "test", repo: "generic"}, version, {bintrayToken: "5df2cadec86dff91392e4c419540785813c3db15"}) try { const artifactName = `icon-${version}.icns` await publisher.upload(iconPath, artifactName) diff --git a/test/src/helpers/fileAssert.ts b/test/src/helpers/fileAssert.ts index c0e43721957..e88ec68c352 100644 --- a/test/src/helpers/fileAssert.ts +++ b/test/src/helpers/fileAssert.ts @@ -37,6 +37,10 @@ class Assertions { compare(this.actual, "", true) } + isNotNull() { + compare(this.actual, null, true) + } + doesNotMatch(pattern: RegExp) { if ((this.actual).match(pattern)) { throw new Error(`${this.actual} matches ${pattern}`) diff --git a/test/src/nsisUpdaterTest.ts b/test/src/nsisUpdaterTest.ts index af09f1e795e..2cf00832347 100644 --- a/test/src/nsisUpdaterTest.ts +++ b/test/src/nsisUpdaterTest.ts @@ -1,11 +1,20 @@ import test from "./helpers/avaEx" +import { assertThat } from "./helpers/fileAssert" +import { NsisUpdater } from "out/nsis-auto-updater/src/nsis-updater" //noinspection JSUnusedLocalSymbols -const __awaiter = require("out/util/awaiter") +const __awaiter = require("out/util/awaiter"); -const updater = require("../../nsis-auto-updater/out/nsis-auto-updater/src/nsis-updater") +(global).__test_app = { + getVersion: function () { + return "0.0.1" + } +} -test("Check updates - no latest version", async (t) => { +const NsisUpdaterClass = require("../../nsis-auto-updater/out/nsis-auto-updater/src/nsis-updater").NsisUpdater + +test("check updates - no versions at all", async (t) => { + const updater: NsisUpdater = new NsisUpdaterClass() //noinspection ReservedWordAsName updater.setFeedURL({ user: "actperepo", @@ -13,4 +22,17 @@ test("Check updates - no latest version", async (t) => { }) t.throws(updater.checkForUpdates(), /No latest version, please ensure that/) +}) + +test("cannot find suitable file for version", async (t) => { + const updater: NsisUpdater = new NsisUpdaterClass() + //noinspection ReservedWordAsName + updater.setFeedURL({ + user: "actperepo", + package: "incorrect-file-version", + }) + + const updateCheckResult = await updater.checkForUpdates() + assertThat(updateCheckResult.downloadPromise).isNotNull() + t.throws(updateCheckResult.downloadPromise, /Cannot find suitable file for version 1.0.0 in/) }) \ No newline at end of file