Skip to content

Commit

Permalink
feat: private GitHub provider
Browse files Browse the repository at this point in the history
Close #1266
  • Loading branch information
AlienHoboken authored and develar committed Mar 13, 2017
1 parent c37bd00 commit ce1df10
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 19 deletions.
6 changes: 6 additions & 0 deletions docs/Auto Update.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ Simplified auto-update is not supported for Squirrel.Windows.
`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`).

## Private Update Repo

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.

**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.

## Debugging

You don't need to listen all events to understand what's wrong. Just set `logger`.
Expand Down
20 changes: 17 additions & 3 deletions packages/electron-updater/src/AppUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import "source-map-support/register"
import BluebirdPromise from "bluebird-lst"
import { executorHolder, RequestHeaders } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
Expand All @@ -8,11 +7,13 @@ import { readFile } from "fs-extra-p"
import { safeLoad } from "js-yaml"
import * as path from "path"
import { gt as isVersionGreaterThan, valid as parseVersion } from "semver"
import "source-map-support/register"
import { FileInfo, Provider, UpdateCheckResult, UpdaterSignal } from "./api"
import { BintrayProvider } from "./BintrayProvider"
import { ElectronHttpExecutor } from "./electronHttpExecutor"
import { GenericProvider } from "./GenericProvider"
import { GitHubProvider } from "./GitHubProvider"
import { PrivateGitHubProvider } from "./PrivateGitHubProvider"

export interface Logger {
info(message?: any): void
Expand Down Expand Up @@ -244,6 +245,14 @@ export abstract class AppUpdater extends EventEmitter {
}
return safeLoad(await readFile(this._appUpdateConfigPath, "utf-8"))
}

protected computeRequestHeaders(fileInfo: FileInfo): RequestHeaders | null {
let requestHeaders = this.requestHeaders
if (fileInfo.headers != null) {
return requestHeaders == null ? fileInfo.headers : Object.assign({}, fileInfo.headers, requestHeaders)
}
return requestHeaders
}
}

function createClient(data: string | PublishConfiguration) {
Expand All @@ -254,8 +263,13 @@ function createClient(data: string | PublishConfiguration) {
const provider = (<PublishConfiguration>data).provider
switch (provider) {
case "github":
return new GitHubProvider(<GithubOptions>data)

if (process.env.GH_TOKEN == null) {
return new GitHubProvider(<GithubOptions>data)
}
else {
return new PrivateGitHubProvider(<GithubOptions>data)
}

case "s3": {
const s3 = <S3Options>data
return new GenericProvider({
Expand Down
18 changes: 5 additions & 13 deletions packages/electron-updater/src/GitHubProvider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Provider, FileInfo, getDefaultChannelName, getChannelFilename, getCurrentPlatform } from "./api"
import { VersionInfo, GithubOptions, UpdateInfo, githubUrl } from "electron-builder-http/out/publishOptions"
import { validateUpdateInfo } from "./GenericProvider"
import * as path from "path"
import { HttpError, request } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { Url, parse as parseUrl, format as buggyFormat } from "url"
import { GithubOptions, githubUrl, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
import { RequestOptions } from "http"
import * as path from "path"
import { parse as parseUrl } from "url"
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
import { validateUpdateInfo } from "./GenericProvider"

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

interface GithubReleaseInfo {
readonly tag_name: string
}

// url.format doesn't correctly use path and requires explicit pathname
function formatUrl(url: Url) {
if (url.path != null && url.pathname == null) {
url.pathname = url.path
}
return buggyFormat(url)
}
2 changes: 1 addition & 1 deletion packages/electron-updater/src/MacUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class MacUpdater extends AppUpdater {
}

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

Expand Down
4 changes: 2 additions & 2 deletions packages/electron-updater/src/NsisUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import "source-map-support/register"
import { spawn } from "child_process"
import { download, DownloadOptions } from "electron-builder-http"
import { CancellationError, CancellationToken } from "electron-builder-http/out/CancellationToken"
import { PublishConfiguration, VersionInfo } from "electron-builder-http/out/publishOptions"
import { mkdtemp, remove } from "fs-extra-p"
import { tmpdir } from "os"
import * as path from "path"
import "source-map-support/register"
import { DOWNLOAD_PROGRESS, FileInfo } from "./api"
import { AppUpdater } from "./AppUpdater"

Expand All @@ -25,7 +25,7 @@ export class NsisUpdater extends AppUpdater {
protected async doDownloadUpdate(versionInfo: VersionInfo, fileInfo: FileInfo, cancellationToken: CancellationToken) {
const downloadOptions: DownloadOptions = {
skipDirCreation: true,
headers: this.requestHeaders || undefined,
headers: this.computeRequestHeaders(fileInfo),
cancellationToken: cancellationToken,
sha2: fileInfo == null ? null : fileInfo.sha2,
}
Expand Down
114 changes: 114 additions & 0 deletions packages/electron-updater/src/PrivateGitHubProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { HttpError, request } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { GithubOptions, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
import { RequestOptions } from "http"
import { safeLoad } from "js-yaml"
import * as path from "path"
import { parse as parseUrl } from "url"
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
import { validateUpdateInfo } from "./GenericProvider"

export class PrivateGitHubProvider extends Provider<VersionInfo> {
// so, we don't need to parse port (because node http doesn't support host as url does)
private readonly baseUrl: RequestOptions
private apiResult: any

constructor(private readonly options: GithubOptions) {
super()

const baseUrl = parseUrl(`${options.protocol || "https"}://${options.host || "api.github.com"}`)
this.baseUrl = {
protocol: baseUrl.protocol,
hostname: baseUrl.hostname,
port: <any>baseUrl.port,
}
}

async getLatestVersion(): Promise<UpdateInfo> {
const basePath = this.getBasePath()
const cancellationToken = new CancellationToken()
let result: any
const channelFile = getChannelFilename(getDefaultChannelName())
const versionUrl = await this.getLatestVersionUrl(basePath, cancellationToken, channelFile)
const assetPath = parseUrl(versionUrl).path
const requestOptions = Object.assign({
path: `${assetPath}?access_token=${process.env.GH_TOKEN}`,
headers: Object.assign({
Accept: "application/octet-stream",
"User-Agent": this.options.owner
}, this.requestHeaders)
}, this.baseUrl)
try {
result = await request<UpdateInfo>(requestOptions, cancellationToken)
//Maybe better to parse in httpExecutor ?
if (typeof result === "string") {
if (getCurrentPlatform() === "darwin") {
result = JSON.parse(result)
}
else {
result = safeLoad(result)
}
}
}
catch (e) {
if (e instanceof HttpError && e.response.statusCode === 404) {
throw new Error(`Cannot find ${channelFile} in the latest release artifacts (${formatUrl(<any>requestOptions)}): ${e.stack || e.message}`)
}
throw e
}

validateUpdateInfo(result)
if (getCurrentPlatform() === "darwin") {
result.releaseJsonUrl = `${this.options.protocol || "https"}://${this.options.host || "api.github.com"}${requestOptions.path}`
}
return result
}

private async getLatestVersionUrl(basePath: string, cancellationToken: CancellationToken, channelFile: string): Promise<string> {
const requestOptions: RequestOptions = Object.assign({
path: `${basePath}/latest?access_token=${process.env.GH_TOKEN}`,
headers: Object.assign({Accept: "application/json", "User-Agent": this.options.owner}, this.requestHeaders)
}, this.baseUrl)
try {
this.apiResult = (await request<any>(requestOptions, cancellationToken))
return this.apiResult.assets.find((elem: any) => {
return elem.name == channelFile
}).url
}
catch (e) {
throw new Error(`Unable to find latest version on GitHub (${formatUrl(<any>requestOptions)}), please ensure a production release exists: ${e.stack || e.message}`)
}
}

private getBasePath() {
return `/repos/${this.options.owner}/${this.options.repo}/releases`
}

async getUpdateFile(versionInfo: UpdateInfo): Promise<FileInfo> {
const headers = {
Accept: "application/octet-stream",
"User-Agent": this.options.owner,
Authorization: `token ${process.env.GH_TOKEN}`
}

// space is not supported on GitHub
if (getCurrentPlatform() === "darwin") {
const info = <any>versionInfo
const name = info.url.split("/").pop()
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
info.url = formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl))
info.headers = headers
return info
}
else {
const name = versionInfo.githubArtifactName || path.posix.basename(versionInfo.path).replace(/ /g, "-")
const assetPath = parseUrl(this.apiResult.assets.find((it: any) => it.name == name).url).path
return {
name: name,
url: formatUrl(Object.assign({path: `${assetPath}`}, this.baseUrl)),
sha2: versionInfo.sha2,
headers: headers,
}
}
}
}
10 changes: 10 additions & 0 deletions packages/electron-updater/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { ProgressInfo } from "electron-builder-http/out/ProgressCallbackTransform"
import { VersionInfo } from "electron-builder-http/out/publishOptions"
import { EventEmitter } from "events"
import { format as buggyFormat, Url } from "url"

export interface FileInfo {
readonly name: string
readonly url: string
readonly sha2?: string
readonly headers?: Object
}

export abstract class Provider<T extends VersionInfo> {
Expand Down Expand Up @@ -83,4 +85,12 @@ function addHandler(emitter: EventEmitter, event: string, handler: Function) {
else {
emitter.on(event, handler)
}
}

// url.format doesn't correctly use path and requires explicit pathname
export function formatUrl(url: Url) {
if (url.path != null && url.pathname == null) {
url.pathname = url.path
}
return buggyFormat(url)
}

0 comments on commit ce1df10

Please sign in to comment.