Skip to content

Commit

Permalink
feat(nsis): multilang installer
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Aug 8, 2016
1 parent d593a61 commit d4b1a8e
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 85 deletions.
67 changes: 60 additions & 7 deletions nsis-auto-updater/src/nsis-updater.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
import { EventEmitter } from "events"
import { app } from "electron"
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 { HttpError } from "../../src/publish/restApiRequest"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("../../src/util/awaiter")

interface VersionInfo {
version: string
}

interface Provider {
checkForUpdates(): Promise<VersionInfo>
}

//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 {
private client: BintrayClient

constructor(configuration: BintraySourceMetadata) {
this.client = new BintrayClient(configuration.user, configuration.package, configuration.repository || "generic")
}

async checkForUpdates(): Promise<VersionInfo> {
try {
const data = await this.client.getVersion("_latest")
return {
version: data.name,
}
}
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 e
}
}
}

class NsisUpdater extends EventEmitter {
private setupPath = path.join(tmpdir(), 'innobox-upgrade.exe')

private updateAvailable = false
private quitAndInstallCalled = false

private client: Provider

constructor(public updateUrl?: string) {
super()
}
Expand All @@ -18,17 +67,21 @@ class NsisUpdater extends EventEmitter {
return this.updateUrl
}

setFeedURL(value: string) {
this.updateUrl = value
setFeedURL(value: string | BintraySourceMetadata) {
this.updateUrl = value.toString()

this.client = new BintrayProvider(<BintraySourceMetadata>value)
}

checkForUpdates(): void {
checkForUpdates(): Promise<any> {
if (this.updateUrl == null) {
this.emitError("Update URL is not set")
return
const message = "Update URL is not set"
this.emitError(message)
return BluebirdPromise.reject(new Error(message))
}

this.emit("checking-for-update")
return this.client.checkForUpdates()
}

quitAndInstall(): void {
Expand All @@ -49,7 +102,7 @@ class NsisUpdater extends EventEmitter {
stdio: "ignore",
}).unref()

app.quit()
require("electron").app.quit()
}

// emit both error object and message, this is to keep compatibility with old APIs
Expand Down
5 changes: 4 additions & 1 deletion nsis-auto-updater/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"files": [
"../node_modules/fs-extra-p/index.d.ts",
"../node_modules/fs-extra-p/bluebird.d.ts",
"../src/util/httpRequest.ts"
"../src/util/httpRequest.ts",
"../src/publish/restApiRequest.ts",
"../src/publish/bintray.ts",
"../src/util/awaiter.ts"
],
"include": [
"src/**/*.ts",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"install-app-deps": "./out/install-app-deps.js"
},
"scripts": {
"compile": "npm run compile-production && npm run compile-test",
"compile": "npm run compile-production && npm run compile-test && npm run compile-updater",
"compile-production": "ts-babel",
"compile-test": "ts-babel test",
"compile-updater": "tsc -p nsis-auto-updater",
Expand Down
34 changes: 9 additions & 25 deletions src/publish/BintrayPublisher.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
import { Publisher, PublishOptions } from "./publisher"
import { Promise as BluebirdPromise } from "bluebird"
import { bintrayRequest, HttpError, doApiRequest, uploadFile } from "./gitHubRequest"
import { HttpError, doApiRequest } from "./restApiRequest"
import { uploadFile } from "./uploader"
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"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("../util/awaiter")

//noinspection ReservedWordAsName
interface Version {
readonly name: string
readonly package: string
}

export class BintrayPublisher implements Publisher {
private _versionPromise: BluebirdPromise<Version>
private readonly auth: string

private basePath: string
private readonly client: BintrayClient

constructor(private user: string, apiKey: string, private version: string, private packageName: string, private repo: string = "generic", private options: PublishOptions = {}) {
this.auth = `Basic ${new Buffer(`${user}:${apiKey}`).toString("base64")}`
this.basePath = `/packages/${this.user}/${this.repo}/${this.packageName}`
this.client = new BintrayClient(user, packageName, repo, apiKey)
this._versionPromise = <BluebirdPromise<Version>>this.init()
}

private async init(): Promise<Version | null> {
try {
return await bintrayRequest<Version>(`${this.basePath}/versions/${this.version}`, this.auth)
return await this.client.getVersion(this.version)
}
catch (e) {
if (e instanceof HttpError && e.response.statusCode === 404) {
if (this.options.publish !== "onTagOrDraft") {
log(`Version ${this.version} doesn't exist, creating one`)
return this.createVersion()
return this.client.createVersion(this.version)
}
else {
log(`Version ${this.version} doesn't exist, artifacts will be not published`)
Expand All @@ -46,12 +40,6 @@ export class BintrayPublisher implements Publisher {
}
}

private createVersion() {
return bintrayRequest<Version>(`${this.basePath}/versions`, this.auth, {
name: this.version,
})
}

async upload(file: string, artifactName?: string): Promise<any> {
const fileName = artifactName || basename(file)
const version = await this._versionPromise
Expand All @@ -74,7 +62,7 @@ export class BintrayPublisher implements Publisher {
"X-Bintray-Override": "1",
"X-Bintray-Publish": "1",
}
}, this.auth, uploadFile.bind(this, file, fileStat, fileName))
}, this.client.auth, uploadFile.bind(this, file, fileStat, fileName))
}
catch (e) {
if (e instanceof HttpError && e.response.statusCode === 502 && badGatewayCount++ < 3) {
Expand All @@ -93,10 +81,6 @@ export class BintrayPublisher implements Publisher {
}

const version = this._versionPromise.value()
if (version == null) {
return BluebirdPromise.resolve()
}

return bintrayRequest<Version>(`/packages/${this.user}/${this.repo}/${this.packageName}/versions/${version.name}`, this.auth, null, "DELETE")
return version == null ? BluebirdPromise.resolve() : this.client.deleteVersion(version.name)
}
}
31 changes: 31 additions & 0 deletions src/publish/bintray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { bintrayRequest } from "./restApiRequest"

//noinspection ReservedWordAsName
export interface Version {
readonly name: string
readonly package: string
}

export class BintrayClient {
private readonly basePath: string
readonly auth: string | null

constructor(private user: string, private packageName: string, private repo: string = "generic", apiKey?: string | null) {
this.auth = apiKey == null ? null : `Basic ${new Buffer(`${user}:${apiKey}`).toString("base64")}`
this.basePath = `/packages/${this.user}/${this.repo}/${this.packageName}`
}

getVersion(version: string): Promise<Version> {
return bintrayRequest<Version>(`${this.basePath}/versions/${version}`, this.auth)
}

createVersion(version: string): Promise<any> {
return bintrayRequest<Version>(`${this.basePath}/versions`, this.auth, {
name: version,
})
}

deleteVersion(version: string): Promise<any> {
return bintrayRequest(`/packages/${this.user}/${this.repo}/${this.packageName}/versions/${version}`, this.auth, null, "DELETE")
}
}
15 changes: 8 additions & 7 deletions src/publish/gitHubPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { basename } from "path"
import { parse as parseUrl } from "url"
import * as mime from "mime"
import { stat } from "fs-extra-p"
import { gitHubRequest, HttpError, doApiRequest, uploadFile } from "./gitHubRequest"
import { githubRequest, HttpError, doApiRequest } from "./restApiRequest"
import { Promise as BluebirdPromise } from "bluebird"
import { PublishPolicy, PublishOptions, Publisher } from "./publisher"
import { uploadFile } from "./uploader"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("../util/awaiter")
Expand Down Expand Up @@ -56,7 +57,7 @@ export class GitHubPublisher implements Publisher {
private async init(): Promise<Release | null> {
const createReleaseIfNotExists = this.policy !== "onTagOrDraft"
// we don't use "Get a release by tag name" because "tag name" means existing git tag, but we draft release and don't create git tag
const releases = await gitHubRequest<Array<Release>>(`/repos/${this.owner}/${this.repo}/releases`, this.token)
const releases = await githubRequest<Array<Release>>(`/repos/${this.owner}/${this.repo}/releases`, this.token)
for (let release of releases) {
if (release.tag_name === this.tag) {
if (release.draft) {
Expand Down Expand Up @@ -121,10 +122,10 @@ export class GitHubPublisher implements Publisher {
if (e.response.statusCode === 422 && e.description != null && e.description.errors != null && e.description.errors[0].code === "already_exists") {
// delete old artifact and re-upload
log(`Artifact ${fileName} already exists, overwrite one`)
const assets = await gitHubRequest<Array<Asset>>(`/repos/${this.owner}/${this.repo}/releases/${release.id}/assets`, this.token)
const assets = await githubRequest<Array<Asset>>(`/repos/${this.owner}/${this.repo}/releases/${release.id}/assets`, this.token)
for (let asset of assets) {
if (asset!.name === fileName) {
await gitHubRequest<void>(`/repos/${this.owner}/${this.repo}/releases/assets/${asset!.id}`, this.token, null, "DELETE")
await githubRequest<void>(`/repos/${this.owner}/${this.repo}/releases/assets/${asset!.id}`, this.token, null, "DELETE")
continue uploadAttempt
}
}
Expand All @@ -143,7 +144,7 @@ export class GitHubPublisher implements Publisher {
}

private createRelease() {
return gitHubRequest<Release>(`/repos/${this.owner}/${this.repo}/releases`, this.token, {
return githubRequest<Release>(`/repos/${this.owner}/${this.repo}/releases`, this.token, {
tag_name: this.tag,
name: this.version,
draft: this.options.draft == null || this.options.draft,
Expand All @@ -154,7 +155,7 @@ export class GitHubPublisher implements Publisher {
// test only
//noinspection JSUnusedGlobalSymbols
async getRelease(): Promise<any> {
return gitHubRequest<Release>(`/repos/${this.owner}/${this.repo}/releases/${this._releasePromise.value().id}`, this.token)
return githubRequest<Release>(`/repos/${this.owner}/${this.repo}/releases/${this._releasePromise.value().id}`, this.token)
}

//noinspection JSUnusedGlobalSymbols
Expand All @@ -170,7 +171,7 @@ export class GitHubPublisher implements Publisher {

for (let i = 0; i < 3; i++) {
try {
return await gitHubRequest(`/repos/${this.owner}/${this.repo}/releases/${release.id}`, this.token, null, "DELETE")
return await githubRequest(`/repos/${this.owner}/${this.repo}/releases/${release.id}`, this.token, null, "DELETE")
}
catch (e) {
if (e instanceof HttpError && (e.response.statusCode === 405 || e.response.statusCode === 502)) {
Expand Down
32 changes: 3 additions & 29 deletions src/publish/gitHubRequest.ts → src/publish/restApiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ import { RequestOptions } from "https"
import { IncomingMessage, ClientRequest } from "http"
import { addTimeOutHandler } from "../util/httpRequest"
import { Promise as BluebirdPromise } from "bluebird"
import { createReadStream, Stats } from "fs-extra-p"
import progressStream = require("progress-stream")
import ProgressBar = require("progress")
import { ReadStream } from "tty"

//noinspection JSUnusedLocalSymbols
const __awaiter = require("../util/awaiter")

export function gitHubRequest<T>(path: string, token: string | null, data: { [name: string]: any; } | null = null, method: string = "GET"): BluebirdPromise<T> {
export function githubRequest<T>(path: string, token: string | null, data: { [name: string]: any; } | null = null, method: string = "GET"): BluebirdPromise<T> {
return request<T>("api.github.com", path, token, data, method)
}

export function bintrayRequest<T>(path: string, token: string | null, data: { [name: string]: any; } | null = null, method: string = "GET"): BluebirdPromise<T> {
return request<T>("api.bintray.com", path, token, data, method)
export function bintrayRequest<T>(path: string, auth: string | null, data: { [name: string]: any; } | null = null, method: string = "GET"): BluebirdPromise<T> {
return request<T>("api.bintray.com", path, auth, data, method)
}

function request<T>(hostname: string, path: string, token: string | null, data: { [name: string]: any; } | null = null, method: string = "GET"): BluebirdPromise<T> {
Expand Down Expand Up @@ -104,26 +100,4 @@ export class HttpError extends Error {
constructor(public response: IncomingMessage, public description: any = null) {
super(response.statusCode + " " + response.statusMessage + (description == null ? "" : ("\n" + JSON.stringify(description, <any>null, " "))) + "\nHeaders: " + JSON.stringify(response.headers, <any>null, " "))
}
}

export function uploadFile(file: string, fileStat: Stats, fileName: string, request: ClientRequest, reject: (error: Error) => void) {
const progressBar = (<ReadStream>process.stdin).isTTY ? new ProgressBar(`Uploading ${fileName} [:bar] :percent :etas`, {
total: fileStat.size,
incomplete: " ",
stream: process.stdout,
width: 20,
}) : null

const fileInputStream = createReadStream(file)
fileInputStream.on("error", reject)
fileInputStream
.pipe(progressStream({
length: fileStat.size,
time: 1000
}, progress => {
if (progressBar != null) {
progressBar.tick(progress.delta)
}
}))
.pipe(request)
}
27 changes: 27 additions & 0 deletions src/publish/uploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import progressStream = require("progress-stream")
import ProgressBar = require("progress")
import { createReadStream, Stats } from "fs-extra-p"
import { ReadStream } from "tty"
import { ClientRequest } from "http"

export function uploadFile(file: string, fileStat: Stats, fileName: string, request: ClientRequest, reject: (error: Error) => void) {
const progressBar = (<ReadStream>process.stdin).isTTY ? new ProgressBar(`Uploading ${fileName} [:bar] :percent :etas`, {
total: fileStat.size,
incomplete: " ",
stream: process.stdout,
width: 20,
}) : null

const fileInputStream = createReadStream(file)
fileInputStream.on("error", reject)
fileInputStream
.pipe(progressStream({
length: fileStat.size,
time: 1000
}, progress => {
if (progressBar != null) {
progressBar.tick(progress.delta)
}
}))
.pipe(request)
}
Loading

0 comments on commit d4b1a8e

Please sign in to comment.