Skip to content

Commit ce1df10

Browse files
AlienHobokendevelar
authored andcommitted
feat: private GitHub provider
Close #1266
1 parent c37bd00 commit ce1df10

File tree

7 files changed

+155
-19
lines changed

7 files changed

+155
-19
lines changed

docs/Auto Update.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ Simplified auto-update is not supported for Squirrel.Windows.
2626
2727
`latest.yml` (or `latest-mac.json` for macOS) will be generated and uploaded for all providers except `bintray` (because not required, `bintray` doesn't use `latest.yml`).
2828

29+
## Private Update Repo
30+
31+
You can use a private repository for updates with electron-updater by setting the `GH_TOKEN` environment variable. If `GH_TOKEN` is set, electron-updater will use the GitHub API for updates allowing private repositories to work.
32+
33+
**Note:** The GitHub API currently has a rate limit of 5000 requests per user per hour. An update check uses up to 3 requests per check. If you are worried about hitting your rate limit, consider using [conditional requests](https://developer.github.com/v3/#conditional-requests) before checking for updates to reduce rate limit usage.
34+
2935
## Debugging
3036

3137
You don't need to listen all events to understand what's wrong. Just set `logger`.

packages/electron-updater/src/AppUpdater.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import "source-map-support/register"
21
import BluebirdPromise from "bluebird-lst"
32
import { executorHolder, RequestHeaders } from "electron-builder-http"
43
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
@@ -8,11 +7,13 @@ import { readFile } from "fs-extra-p"
87
import { safeLoad } from "js-yaml"
98
import * as path from "path"
109
import { gt as isVersionGreaterThan, valid as parseVersion } from "semver"
10+
import "source-map-support/register"
1111
import { FileInfo, Provider, UpdateCheckResult, UpdaterSignal } from "./api"
1212
import { BintrayProvider } from "./BintrayProvider"
1313
import { ElectronHttpExecutor } from "./electronHttpExecutor"
1414
import { GenericProvider } from "./GenericProvider"
1515
import { GitHubProvider } from "./GitHubProvider"
16+
import { PrivateGitHubProvider } from "./PrivateGitHubProvider"
1617

1718
export interface Logger {
1819
info(message?: any): void
@@ -244,6 +245,14 @@ export abstract class AppUpdater extends EventEmitter {
244245
}
245246
return safeLoad(await readFile(this._appUpdateConfigPath, "utf-8"))
246247
}
248+
249+
protected computeRequestHeaders(fileInfo: FileInfo): RequestHeaders | null {
250+
let requestHeaders = this.requestHeaders
251+
if (fileInfo.headers != null) {
252+
return requestHeaders == null ? fileInfo.headers : Object.assign({}, fileInfo.headers, requestHeaders)
253+
}
254+
return requestHeaders
255+
}
247256
}
248257

249258
function createClient(data: string | PublishConfiguration) {
@@ -254,8 +263,13 @@ function createClient(data: string | PublishConfiguration) {
254263
const provider = (<PublishConfiguration>data).provider
255264
switch (provider) {
256265
case "github":
257-
return new GitHubProvider(<GithubOptions>data)
258-
266+
if (process.env.GH_TOKEN == null) {
267+
return new GitHubProvider(<GithubOptions>data)
268+
}
269+
else {
270+
return new PrivateGitHubProvider(<GithubOptions>data)
271+
}
272+
259273
case "s3": {
260274
const s3 = <S3Options>data
261275
return new GenericProvider({

packages/electron-updater/src/GitHubProvider.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Provider, FileInfo, getDefaultChannelName, getChannelFilename, getCurrentPlatform } from "./api"
2-
import { VersionInfo, GithubOptions, UpdateInfo, githubUrl } from "electron-builder-http/out/publishOptions"
3-
import { validateUpdateInfo } from "./GenericProvider"
4-
import * as path from "path"
51
import { HttpError, request } from "electron-builder-http"
62
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
7-
import { Url, parse as parseUrl, format as buggyFormat } from "url"
3+
import { GithubOptions, githubUrl, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
84
import { RequestOptions } from "http"
5+
import * as path from "path"
6+
import { parse as parseUrl } from "url"
7+
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
8+
import { validateUpdateInfo } from "./GenericProvider"
99

1010
export class GitHubProvider extends Provider<VersionInfo> {
1111
// so, we don't need to parse port (because node http doesn't support host as url does)
@@ -83,12 +83,4 @@ export class GitHubProvider extends Provider<VersionInfo> {
8383

8484
interface GithubReleaseInfo {
8585
readonly tag_name: string
86-
}
87-
88-
// url.format doesn't correctly use path and requires explicit pathname
89-
function formatUrl(url: Url) {
90-
if (url.path != null && url.pathname == null) {
91-
url.pathname = url.path
92-
}
93-
return buggyFormat(url)
9486
}

packages/electron-updater/src/MacUpdater.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class MacUpdater extends AppUpdater {
2626
}
2727

2828
protected onUpdateAvailable(versionInfo: VersionInfo, fileInfo: FileInfo) {
29-
this.nativeUpdater.setFeedURL((<any>versionInfo).releaseJsonUrl, Object.assign({"Cache-Control": "no-cache"}, this.requestHeaders))
29+
this.nativeUpdater.setFeedURL((<any>versionInfo).releaseJsonUrl, Object.assign({"Cache-Control": "no-cache"}, this.computeRequestHeaders(fileInfo)))
3030
super.onUpdateAvailable(versionInfo, fileInfo)
3131
}
3232

packages/electron-updater/src/NsisUpdater.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import "source-map-support/register"
21
import { spawn } from "child_process"
32
import { download, DownloadOptions } from "electron-builder-http"
43
import { CancellationError, CancellationToken } from "electron-builder-http/out/CancellationToken"
54
import { PublishConfiguration, VersionInfo } from "electron-builder-http/out/publishOptions"
65
import { mkdtemp, remove } from "fs-extra-p"
76
import { tmpdir } from "os"
87
import * as path from "path"
8+
import "source-map-support/register"
99
import { DOWNLOAD_PROGRESS, FileInfo } from "./api"
1010
import { AppUpdater } from "./AppUpdater"
1111

@@ -25,7 +25,7 @@ export class NsisUpdater extends AppUpdater {
2525
protected async doDownloadUpdate(versionInfo: VersionInfo, fileInfo: FileInfo, cancellationToken: CancellationToken) {
2626
const downloadOptions: DownloadOptions = {
2727
skipDirCreation: true,
28-
headers: this.requestHeaders || undefined,
28+
headers: this.computeRequestHeaders(fileInfo),
2929
cancellationToken: cancellationToken,
3030
sha2: fileInfo == null ? null : fileInfo.sha2,
3131
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { HttpError, request } from "electron-builder-http"
2+
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
3+
import { GithubOptions, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
4+
import { RequestOptions } from "http"
5+
import { safeLoad } from "js-yaml"
6+
import * as path from "path"
7+
import { parse as parseUrl } from "url"
8+
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
9+
import { validateUpdateInfo } from "./GenericProvider"
10+
11+
export class PrivateGitHubProvider extends Provider<VersionInfo> {
12+
// so, we don't need to parse port (because node http doesn't support host as url does)
13+
private readonly baseUrl: RequestOptions
14+
private apiResult: any
15+
16+
constructor(private readonly options: GithubOptions) {
17+
super()
18+
19+
const baseUrl = parseUrl(`${options.protocol || "https"}://${options.host || "api.github.com"}`)
20+
this.baseUrl = {
21+
protocol: baseUrl.protocol,
22+
hostname: baseUrl.hostname,
23+
port: <any>baseUrl.port,
24+
}
25+
}
26+
27+
async getLatestVersion(): Promise<UpdateInfo> {
28+
const basePath = this.getBasePath()
29+
const cancellationToken = new CancellationToken()
30+
let result: any
31+
const channelFile = getChannelFilename(getDefaultChannelName())
32+
const versionUrl = await this.getLatestVersionUrl(basePath, cancellationToken, channelFile)
33+
const assetPath = parseUrl(versionUrl).path
34+
const requestOptions = Object.assign({
35+
path: `${assetPath}?access_token=${process.env.GH_TOKEN}`,
36+
headers: Object.assign({
37+
Accept: "application/octet-stream",
38+
"User-Agent": this.options.owner
39+
}, this.requestHeaders)
40+
}, this.baseUrl)
41+
try {
42+
result = await request<UpdateInfo>(requestOptions, cancellationToken)
43+
//Maybe better to parse in httpExecutor ?
44+
if (typeof result === "string") {
45+
if (getCurrentPlatform() === "darwin") {
46+
result = JSON.parse(result)
47+
}
48+
else {
49+
result = safeLoad(result)
50+
}
51+
}
52+
}
53+
catch (e) {
54+
if (e instanceof HttpError && e.response.statusCode === 404) {
55+
throw new Error(`Cannot find ${channelFile} in the latest release artifacts (${formatUrl(<any>requestOptions)}): ${e.stack || e.message}`)
56+
}
57+
throw e
58+
}
59+
60+
validateUpdateInfo(result)
61+
if (getCurrentPlatform() === "darwin") {
62+
result.releaseJsonUrl = `${this.options.protocol || "https"}://${this.options.host || "api.github.com"}${requestOptions.path}`
63+
}
64+
return result
65+
}
66+
67+
private async getLatestVersionUrl(basePath: string, cancellationToken: CancellationToken, channelFile: string): Promise<string> {
68+
const requestOptions: RequestOptions = Object.assign({
69+
path: `${basePath}/latest?access_token=${process.env.GH_TOKEN}`,
70+
headers: Object.assign({Accept: "application/json", "User-Agent": this.options.owner}, this.requestHeaders)
71+
}, this.baseUrl)
72+
try {
73+
this.apiResult = (await request<any>(requestOptions, cancellationToken))
74+
return this.apiResult.assets.find((elem: any) => {
75+
return elem.name == channelFile
76+
}).url
77+
}
78+
catch (e) {
79+
throw new Error(`Unable to find latest version on GitHub (${formatUrl(<any>requestOptions)}), please ensure a production release exists: ${e.stack || e.message}`)
80+
}
81+
}
82+
83+
private getBasePath() {
84+
return `/repos/${this.options.owner}/${this.options.repo}/releases`
85+
}
86+
87+
async getUpdateFile(versionInfo: UpdateInfo): Promise<FileInfo> {
88+
const headers = {
89+
Accept: "application/octet-stream",
90+
"User-Agent": this.options.owner,
91+
Authorization: `token ${process.env.GH_TOKEN}`
92+
}
93+
94+
// space is not supported on GitHub
95+
if (getCurrentPlatform() === "darwin") {
96+
const info = <any>versionInfo
97+
const name = info.url.split("/").pop()
98+
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
99+
info.url = formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl))
100+
info.headers = headers
101+
return info
102+
}
103+
else {
104+
const name = versionInfo.githubArtifactName || path.posix.basename(versionInfo.path).replace(/ /g, "-")
105+
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
106+
return {
107+
name: name,
108+
url: formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl)),
109+
sha2: versionInfo.sha2,
110+
headers: headers,
111+
}
112+
}
113+
}
114+
}

packages/electron-updater/src/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { CancellationToken } from "electron-builder-http/out/CancellationToken"
33
import { ProgressInfo } from "electron-builder-http/out/ProgressCallbackTransform"
44
import { VersionInfo } from "electron-builder-http/out/publishOptions"
55
import { EventEmitter } from "events"
6+
import { format as buggyFormat, Url } from "url"
67

78
export interface FileInfo {
89
readonly name: string
910
readonly url: string
1011
readonly sha2?: string
12+
readonly headers?: Object
1113
}
1214

1315
export abstract class Provider<T extends VersionInfo> {
@@ -83,4 +85,12 @@ function addHandler(emitter: EventEmitter, event: string, handler: Function) {
8385
else {
8486
emitter.on(event, handler)
8587
}
88+
}
89+
90+
// url.format doesn't correctly use path and requires explicit pathname
91+
export function formatUrl(url: Url) {
92+
if (url.path != null && url.pathname == null) {
93+
url.pathname = url.path
94+
}
95+
return buggyFormat(url)
8696
}

0 commit comments

Comments
 (0)