Skip to content

Commit a24f12c

Browse files
committed
feat: cancellable and progressable publishing
1 parent fd4fc0f commit a24f12c

33 files changed

+456
-277
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@
3333
"electron-download-tf": "3.2.0",
3434
"electron-macos-sign": "~1.6.0",
3535
"fs-extra-p": "^3.1.0",
36-
"hosted-git-info": "^2.1.5",
36+
"hosted-git-info": "^2.2.0",
3737
"ini": "^1.3.4",
3838
"is-ci": "^1.0.10",
3939
"isbinaryfile": "^3.0.2",
40-
"js-yaml": "^3.7.0",
40+
"js-yaml": "^3.8.1",
4141
"mime": "^1.3.4",
4242
"minimatch": "^3.0.3",
4343
"node-emoji": "^1.5.1",
4444
"normalize-package-data": "^2.3.5",
4545
"parse-color": "^1.0.0",
4646
"plist": "^2.0.1",
47-
"progress": "^1.1.8",
47+
"progress-ex": "^2.0.0",
4848
"sanitize-filename": "^1.6.1",
4949
"semver": "^5.3.0",
5050
"stat-mode": "^0.2.2",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { EventEmitter } from "events"
2+
import BluebirdPromise from "bluebird-lst-c"
3+
4+
export class CancellationToken extends EventEmitter {
5+
private _cancelled = false
6+
7+
get cancelled(): boolean {
8+
return this._cancelled || (this._parent != null && this._parent.cancelled)
9+
}
10+
11+
private _parent: CancellationToken | null
12+
set parent(value: CancellationToken) {
13+
this._parent = value
14+
}
15+
16+
cancel() {
17+
this._cancelled = true
18+
this.emit("cancel")
19+
}
20+
21+
onCancel(handler: () => any) {
22+
this.once("cancel", handler)
23+
}
24+
25+
trackPromise(promise: BluebirdPromise<any>): BluebirdPromise<any> {
26+
const handler = () => promise.cancel()
27+
this.onCancel(handler)
28+
// it is important to return promise, otherwise will be unhandled rejection error on reject
29+
return promise.finally(() => this.removeListener("cancel", handler))
30+
}
31+
}

packages/electron-builder-http/src/ProgressCallbackTransform.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Transform } from "stream"
2+
import { CancellationToken } from "./CancellationToken"
23

34
export interface ProgressInfo {
45
total: number
@@ -15,11 +16,16 @@ export class ProgressCallbackTransform extends Transform {
1516

1617
private nextUpdate = this.start + 1000
1718

18-
constructor(private total: number, private onProgress: (info: ProgressInfo) => any) {
19+
constructor(private readonly total: number, private readonly cancellationToken: CancellationToken, private readonly onProgress: (info: ProgressInfo) => any) {
1920
super()
2021
}
2122

2223
_transform(chunk: any, encoding: string, callback: Function) {
24+
if (this.cancellationToken.cancelled) {
25+
callback(new Error("Cancelled"), null)
26+
return
27+
}
28+
2329
this.transferred += chunk.length
2430
this.delta += chunk.length
2531

@@ -41,6 +47,11 @@ export class ProgressCallbackTransform extends Transform {
4147
}
4248

4349
_flush(callback: Function): void {
50+
if (this.cancellationToken.cancelled) {
51+
callback(new Error("Cancelled"))
52+
return
53+
}
54+
4455
this.onProgress(<ProgressInfo>{
4556
total: this.total,
4657
delta: this.delta,

packages/electron-builder-http/src/bintray.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { BintrayOptions } from "./publishOptions"
22
import { request, configureRequestOptions } from "./httpExecutor"
3+
import { CancellationToken } from "./CancellationToken"
34

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

89
export interface Version {
@@ -25,10 +26,10 @@ export class BintrayClient {
2526
readonly repo: string
2627

2728
readonly owner: string
28-
private readonly user: string
29+
readonly user: string
2930
readonly packageName: string
3031

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

4748
getVersion(version: string): Promise<Version> {
48-
return bintrayRequest<Version>(`${this.basePath}/versions/${version}`, this.auth)
49+
return bintrayRequest<Version>(`${this.basePath}/versions/${version}`, this.auth, null, this.cancellationToken)
4950
}
5051

5152
getVersionFiles(version: string): Promise<Array<File>> {
52-
return bintrayRequest<Array<File>>(`${this.basePath}/versions/${version}/files`, this.auth)
53+
return bintrayRequest<Array<File>>(`${this.basePath}/versions/${version}/files`, this.auth, null, this.cancellationToken)
5354
}
5455

5556
createVersion(version: string): Promise<any> {
5657
return bintrayRequest<Version>(`${this.basePath}/versions`, this.auth, {
5758
name: version,
58-
})
59+
}, this.cancellationToken)
5960
}
6061

6162
deleteVersion(version: string): Promise<any> {
62-
return bintrayRequest(`${this.basePath}/versions/${version}`, this.auth, null, "DELETE")
63+
return bintrayRequest(`${this.basePath}/versions/${version}`, this.auth, null, this.cancellationToken, "DELETE")
6364
}
6465
}

packages/electron-builder-http/src/httpExecutor.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ProgressCallbackTransform } from "./ProgressCallbackTransform"
88
import { safeLoad } from "js-yaml"
99
import { EventEmitter } from "events"
1010
import { Socket } from "net"
11+
import { CancellationToken } from "./CancellationToken"
1112

1213
export interface RequestHeaders {
1314
[key: string]: any
@@ -23,9 +24,12 @@ export interface Response extends EventEmitter {
2324
}
2425

2526
export interface DownloadOptions {
26-
headers?: RequestHeaders | null
27-
skipDirCreation?: boolean
28-
sha2?: string
27+
readonly headers?: RequestHeaders | null
28+
readonly skipDirCreation?: boolean
29+
readonly sha2?: string | null
30+
31+
readonly cancellationToken: CancellationToken
32+
2933
onProgress?(progress: any): void
3034
}
3135

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

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

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

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

80-
protected handleResponse(response: Response, options: RequestOptions, resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void) {
84+
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) {
8185
if (this.debug.enabled) {
8286
this.debug(`Response status: ${response.statusCode} ${response.statusMessage}, request options: ${dumpRequestOptions(options)}`)
8387
}
@@ -104,7 +108,7 @@ export abstract class HttpExecutor<REQUEST_OPTS, REQUEST> {
104108
return
105109
}
106110

107-
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), requestProcessor, redirectCount)
111+
this.doApiRequest(<REQUEST_OPTS>Object.assign({}, options, parseUrl(redirectUrl)), cancellationToken, requestProcessor, redirectCount)
108112
.then(resolve)
109113
.catch(reject)
110114

@@ -203,8 +207,8 @@ class DigestTransform extends Transform {
203207
}
204208
}
205209

206-
export function request<T>(options: RequestOptions, data?: {[name: string]: any; } | null): Promise<T> {
207-
return executorHolder.httpExecutor.request(options, data)
210+
export function request<T>(options: RequestOptions, cancellationToken: CancellationToken, data?: {[name: string]: any; } | null): Promise<T> {
211+
return executorHolder.httpExecutor.request(options, cancellationToken, data)
208212
}
209213

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

packages/electron-builder-publisher/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
],
1313
"dependencies": {
1414
"fs-extra-p": "^3.1.0",
15-
"progress": "^1.1.8",
1615
"mime": "^1.3.4",
1716
"bluebird-lst-c": "^1.0.6",
1817
"electron-builder-http": "~0.0.0-semantic-release",
19-
"electron-builder-util": "~0.0.0-semantic-release"
18+
"electron-builder-util": "~0.0.0-semantic-release",
19+
"chalk": "^1.1.3",
20+
"progress-ex": "^2.0.0"
2021
},
2122
"typings": "./out/electron-builder-publisher.d.ts"
2223
}

packages/electron-builder-publisher/src/BintrayPublisher.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import { debug, isEmptyOrSpaces } from "electron-builder-util"
66
import { log } from "electron-builder-util/out/log"
77
import { httpExecutor } from "electron-builder-util/out/nodeHttpExecutor"
88
import { ClientRequest } from "http"
9-
import { Publisher, PublishOptions } from "./publisher"
9+
import { HttpPublisher, PublishContext, PublishOptions } from "./publisher"
1010

11-
export class BintrayPublisher extends Publisher {
11+
export class BintrayPublisher extends HttpPublisher {
1212
private _versionPromise: BluebirdPromise<Version>
1313

1414
private readonly client: BintrayClient
1515

16-
constructor(info: BintrayOptions, private readonly version: string, private readonly options: PublishOptions = {}) {
17-
super()
16+
readonly providerName = "Bintray"
17+
18+
constructor(context: PublishContext, info: BintrayOptions, private readonly version: string, private readonly options: PublishOptions = {}) {
19+
super(context)
1820

1921
let token = info.token
2022
if (isEmptyOrSpaces(token)) {
@@ -24,7 +26,7 @@ export class BintrayPublisher extends Publisher {
2426
}
2527
}
2628

27-
this.client = new BintrayClient(info, token)
29+
this.client = new BintrayClient(info, this.context.cancellationToken, token)
2830
this._versionPromise = <BluebirdPromise<Version>>this.init()
2931
}
3032

@@ -66,7 +68,7 @@ export class BintrayPublisher extends Publisher {
6668
"X-Bintray-Override": "1",
6769
"X-Bintray-Publish": "1",
6870
}
69-
}, this.client.auth), requestProcessor)
71+
}, this.client.auth), this.context.cancellationToken, requestProcessor)
7072
}
7173
catch (e) {
7274
if (e instanceof HttpError && e.response.statusCode === 502 && badGatewayCount++ < 3) {
@@ -87,4 +89,8 @@ export class BintrayPublisher extends Publisher {
8789
const version = this._versionPromise.value()
8890
return version == null ? BluebirdPromise.resolve() : this.client.deleteVersion(version.name)
8991
}
92+
93+
toString() {
94+
return `Bintray (user: ${this.client.user || this.client.owner}, owner: ${this.client.owner}, package: ${this.client.packageName}, repository: ${this.client.repo}, version: ${this.version})`
95+
}
9096
}

0 commit comments

Comments
 (0)