Skip to content

Commit

Permalink
feat: cancellable and progressable publishing
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Feb 7, 2017
1 parent fd4fc0f commit a24f12c
Show file tree
Hide file tree
Showing 33 changed files with 456 additions and 277 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@
"electron-download-tf": "3.2.0",
"electron-macos-sign": "~1.6.0",
"fs-extra-p": "^3.1.0",
"hosted-git-info": "^2.1.5",
"hosted-git-info": "^2.2.0",
"ini": "^1.3.4",
"is-ci": "^1.0.10",
"isbinaryfile": "^3.0.2",
"js-yaml": "^3.7.0",
"js-yaml": "^3.8.1",
"mime": "^1.3.4",
"minimatch": "^3.0.3",
"node-emoji": "^1.5.1",
"normalize-package-data": "^2.3.5",
"parse-color": "^1.0.0",
"plist": "^2.0.1",
"progress": "^1.1.8",
"progress-ex": "^2.0.0",
"sanitize-filename": "^1.6.1",
"semver": "^5.3.0",
"stat-mode": "^0.2.2",
Expand Down
31 changes: 31 additions & 0 deletions packages/electron-builder-http/src/CancellationToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { EventEmitter } from "events"
import BluebirdPromise from "bluebird-lst-c"

export class CancellationToken extends EventEmitter {
private _cancelled = false

get cancelled(): boolean {
return this._cancelled || (this._parent != null && this._parent.cancelled)
}

private _parent: CancellationToken | null
set parent(value: CancellationToken) {
this._parent = value
}

cancel() {
this._cancelled = true
this.emit("cancel")
}

onCancel(handler: () => any) {
this.once("cancel", handler)
}

trackPromise(promise: BluebirdPromise<any>): BluebirdPromise<any> {
const handler = () => promise.cancel()
this.onCancel(handler)
// it is important to return promise, otherwise will be unhandled rejection error on reject
return promise.finally(() => this.removeListener("cancel", handler))
}
}
13 changes: 12 additions & 1 deletion packages/electron-builder-http/src/ProgressCallbackTransform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Transform } from "stream"
import { CancellationToken } from "./CancellationToken"

export interface ProgressInfo {
total: number
Expand All @@ -15,11 +16,16 @@ export class ProgressCallbackTransform extends Transform {

private nextUpdate = this.start + 1000

constructor(private total: number, private onProgress: (info: ProgressInfo) => any) {
constructor(private readonly total: number, private readonly cancellationToken: CancellationToken, private readonly onProgress: (info: ProgressInfo) => any) {
super()
}

_transform(chunk: any, encoding: string, callback: Function) {
if (this.cancellationToken.cancelled) {
callback(new Error("Cancelled"), null)
return
}

this.transferred += chunk.length
this.delta += chunk.length

Expand All @@ -41,6 +47,11 @@ export class ProgressCallbackTransform extends Transform {
}

_flush(callback: Function): void {
if (this.cancellationToken.cancelled) {
callback(new Error("Cancelled"))
return
}

this.onProgress(<ProgressInfo>{
total: this.total,
delta: this.delta,
Expand Down
17 changes: 9 additions & 8 deletions packages/electron-builder-http/src/bintray.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { BintrayOptions } from "./publishOptions"
import { request, configureRequestOptions } from "./httpExecutor"
import { CancellationToken } from "./CancellationToken"

export function bintrayRequest<T>(path: string, auth: string | null, data: {[name: string]: any; } | null = null, method?: "GET" | "DELETE" | "PUT"): Promise<T> {
return request<T>(configureRequestOptions({hostname: "api.bintray.com", path: path}, auth, method), data)
export function bintrayRequest<T>(path: string, auth: string | null, data: {[name: string]: any; } | null = null, cancellationToken: CancellationToken, method?: "GET" | "DELETE" | "PUT"): Promise<T> {
return request<T>(configureRequestOptions({hostname: "api.bintray.com", path: path}, auth, method), cancellationToken, data)
}

export interface Version {
Expand All @@ -25,10 +26,10 @@ export class BintrayClient {
readonly repo: string

readonly owner: string
private readonly user: string
readonly user: string
readonly packageName: string

constructor(options: BintrayOptions, apiKey?: string | null) {
constructor(options: BintrayOptions, private readonly cancellationToken: CancellationToken, apiKey?: string | null) {
if (options.owner == null) {
throw new Error("owner is not specified")
}
Expand All @@ -45,20 +46,20 @@ export class BintrayClient {
}

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

getVersionFiles(version: string): Promise<Array<File>> {
return bintrayRequest<Array<File>>(`${this.basePath}/versions/${version}/files`, this.auth)
return bintrayRequest<Array<File>>(`${this.basePath}/versions/${version}/files`, this.auth, null, this.cancellationToken)
}

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

deleteVersion(version: string): Promise<any> {
return bintrayRequest(`${this.basePath}/versions/${version}`, this.auth, null, "DELETE")
return bintrayRequest(`${this.basePath}/versions/${version}`, this.auth, null, this.cancellationToken, "DELETE")
}
}
26 changes: 15 additions & 11 deletions packages/electron-builder-http/src/httpExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ProgressCallbackTransform } from "./ProgressCallbackTransform"
import { safeLoad } from "js-yaml"
import { EventEmitter } from "events"
import { Socket } from "net"
import { CancellationToken } from "./CancellationToken"

export interface RequestHeaders {
[key: string]: any
Expand All @@ -23,9 +24,12 @@ export interface Response extends EventEmitter {
}

export interface DownloadOptions {
headers?: RequestHeaders | null
skipDirCreation?: boolean
sha2?: string
readonly headers?: RequestHeaders | null
readonly skipDirCreation?: boolean
readonly sha2?: string | null

readonly cancellationToken: CancellationToken

onProgress?(progress: any): void
}

Expand Down Expand Up @@ -62,22 +66,22 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
protected readonly maxRedirects = 10
protected readonly debug = _debug("electron-builder")

request<T>(options: RequestOptions, data?: { [name: string]: any; } | null): Promise<T> {
request<T>(options: RequestOptions, cancellationToken: CancellationToken, data?: { [name: string]: any; } | null): Promise<T> {
configureRequestOptions(options)
const encodedData = data == null ? undefined : new Buffer(JSON.stringify(data))
if (encodedData != null) {
options.method = "post"
options.headers!["Content-Type"] = "application/json"
options.headers!["Content-Length"] = encodedData.length
}
return this.doApiRequest<T>(<REQUEST_OPTS>options, it => (<any>it).end(encodedData), 0)
return this.doApiRequest<T>(<REQUEST_OPTS>options, cancellationToken, it => (<any>it).end(encodedData), 0)
}

protected abstract doApiRequest<T>(options: REQUEST_OPTS, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void, redirectCount: number): Promise<T>
protected abstract doApiRequest<T>(options: REQUEST_OPTS, cancellationToken: CancellationToken, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void, redirectCount: number): Promise<T>

abstract download(url: string, destination: string, options?: DownloadOptions | null): Promise<string>

protected handleResponse(response: Response, options: RequestOptions, resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void) {
protected handleResponse(response: Response, options: RequestOptions, cancellationToken: CancellationToken, resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void) {
if (this.debug.enabled) {
this.debug(`Response status: ${response.statusCode} ${response.statusMessage}, request options: ${dumpRequestOptions(options)}`)
}
Expand All @@ -104,7 +108,7 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
return
}

this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), requestProcessor, redirectCount)
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), cancellationToken, requestProcessor, redirectCount)
.then(resolve)
.catch(reject)

Expand Down Expand Up @@ -203,8 +207,8 @@ class DigestTransform extends Transform {
}
}

export function request<T>(options: RequestOptions, data?: {[name: string]: any; } | null): Promise<T> {
return executorHolder.httpExecutor.request(options, data)
export function request<T>(options: RequestOptions, cancellationToken: CancellationToken, data?: {[name: string]: any; } | null): Promise<T> {
return executorHolder.httpExecutor.request(options, cancellationToken, data)
}

function checkSha2(sha2Header: string | null | undefined, sha2: string | null | undefined, callback: (error: Error | null) => void): boolean {
Expand Down Expand Up @@ -245,7 +249,7 @@ function configurePipes(options: DownloadOptions, response: any, destination: st
if (options.onProgress != null) {
const contentLength = safeGetHeader(response, "content-length")
if (contentLength != null) {
streams.push(new ProgressCallbackTransform(parseInt(contentLength, 10), options.onProgress))
streams.push(new ProgressCallbackTransform(parseInt(contentLength, 10), options.cancellationToken, options.onProgress))
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/electron-builder-publisher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
],
"dependencies": {
"fs-extra-p": "^3.1.0",
"progress": "^1.1.8",
"mime": "^1.3.4",
"bluebird-lst-c": "^1.0.6",
"electron-builder-http": "~0.0.0-semantic-release",
"electron-builder-util": "~0.0.0-semantic-release"
"electron-builder-util": "~0.0.0-semantic-release",
"chalk": "^1.1.3",
"progress-ex": "^2.0.0"
},
"typings": "./out/electron-builder-publisher.d.ts"
}
18 changes: 12 additions & 6 deletions packages/electron-builder-publisher/src/BintrayPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import { debug, isEmptyOrSpaces } from "electron-builder-util"
import { log } from "electron-builder-util/out/log"
import { httpExecutor } from "electron-builder-util/out/nodeHttpExecutor"
import { ClientRequest } from "http"
import { Publisher, PublishOptions } from "./publisher"
import { HttpPublisher, PublishContext, PublishOptions } from "./publisher"

export class BintrayPublisher extends Publisher {
export class BintrayPublisher extends HttpPublisher {
private _versionPromise: BluebirdPromise<Version>

private readonly client: BintrayClient

constructor(info: BintrayOptions, private readonly version: string, private readonly options: PublishOptions = {}) {
super()
readonly providerName = "Bintray"

constructor(context: PublishContext, info: BintrayOptions, private readonly version: string, private readonly options: PublishOptions = {}) {
super(context)

let token = info.token
if (isEmptyOrSpaces(token)) {
Expand All @@ -24,7 +26,7 @@ export class BintrayPublisher extends Publisher {
}
}

this.client = new BintrayClient(info, token)
this.client = new BintrayClient(info, this.context.cancellationToken, token)
this._versionPromise = <BluebirdPromise<Version>>this.init()
}

Expand Down Expand Up @@ -66,7 +68,7 @@ export class BintrayPublisher extends Publisher {
"X-Bintray-Override": "1",
"X-Bintray-Publish": "1",
}
}, this.client.auth), requestProcessor)
}, this.client.auth), this.context.cancellationToken, requestProcessor)
}
catch (e) {
if (e instanceof HttpError && e.response.statusCode === 502 && badGatewayCount++ < 3) {
Expand All @@ -87,4 +89,8 @@ export class BintrayPublisher extends Publisher {
const version = this._versionPromise.value()
return version == null ? BluebirdPromise.resolve() : this.client.deleteVersion(version.name)
}

toString() {
return `Bintray (user: ${this.client.user || this.client.owner}, owner: ${this.client.owner}, package: ${this.client.packageName}, repository: ${this.client.repo}, version: ${this.version})`
}
}
Loading

0 comments on commit a24f12c

Please sign in to comment.