diff --git a/app/auto-updater-linux.js b/app/auto-updater-linux.js new file mode 100644 index 00000000..80c2b057 --- /dev/null +++ b/app/auto-updater-linux.js @@ -0,0 +1,50 @@ +'use strict'; + +const fetch = require('node-fetch'); +const {EventEmitter} = require('events'); + +class AutoUpdater extends EventEmitter { + quitAndInstall() { + this.emitError('QuitAndInstall unimplemented'); + } + getFeedURL() { + return this.updateURL; + } + + setFeedURL(updateURL) { + this.updateURL = updateURL; + } + + checkForUpdates() { + if (!this.updateURL) { + return this.emitError('Update URL is not set'); + } + this.emit('checking-for-update'); + + fetch(this.updateURL) + .then(res => { + if (res.status === 204) { + return this.emit('update-not-available'); + } + return res.json().then(({name, notes, pub_date}) => { + // Only name is mandatory, needed to construct release URL. + if (!name) { + throw new Error('Malformed server response: release name is missing.'); + } + // If `null` is passed to Date constructor, current time will be used. This doesn't work with `undefined` + const date = new Date(pub_date || null); + this.emit('update-available', {}, notes, name, date); + }); + }) + .catch(this.emitError.bind(this)); + } + + emitError(error) { + if (typeof error === 'string') { + error = new Error(error); + } + this.emit('error', error, error.message); + } +} + +module.exports = new AutoUpdater(); diff --git a/app/ui/window.js b/app/ui/window.js index 1b697a06..f9760836 100644 --- a/app/ui/window.js +++ b/app/ui/window.js @@ -4,7 +4,7 @@ const {parse: parseUrl} = require('url'); const uuid = require('uuid'); const fileUriToPath = require('file-uri-to-path'); const isDev = require('electron-is-dev'); -const AutoUpdater = require('../auto-updater'); +const updater = require('../updater'); const toElectronBackgroundColor = require('../utils/to-electron-background-color'); const {icon, cfgDir} = require('../config/paths'); const createRPC = require('../rpc'); @@ -76,8 +76,8 @@ module.exports = class Window { delete app.windowCallback; fetchNotifications(window); // auto updates - if (!isDev && process.platform !== 'linux') { - AutoUpdater(window); + if (!isDev) { + updater(window); } else { //eslint-disable-next-line no-console console.log('ignoring auto updates during dev'); diff --git a/app/auto-updater.js b/app/updater.js similarity index 59% rename from app/auto-updater.js rename to app/updater.js index c3703291..461f4e7d 100644 --- a/app/auto-updater.js +++ b/app/updater.js @@ -1,5 +1,6 @@ // Packages -const {autoUpdater, app} = require('electron'); +const electron = require('electron'); +const {app} = electron; const ms = require('ms'); const retry = require('async-retry'); @@ -7,17 +8,20 @@ const retry = require('async-retry'); // eslint-disable-next-line no-unused-vars const notify = require('./notify'); const {version} = require('./package'); -const {getConfig} = require('./config'); +const {getDecoratedConfig} = require('./plugins'); const {platform} = process; +const isLinux = platform === 'linux'; + +const autoUpdater = isLinux ? require('./auto-updater-linux') : electron.autoUpdater; let isInit = false; // Default to the "stable" update channel let canaryUpdates = false; -const buildFeedUrl = canary => { +const buildFeedUrl = (canary, currentVersion) => { const updatePrefix = canary ? 'releases-canary' : 'releases'; - return `https://${updatePrefix}.hyper.is/update/${platform}`; + return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`; }; const isCanary = updateChannel => updateChannel === 'canary'; @@ -29,7 +33,7 @@ async function init() { }); const config = await retry(async () => { - const content = await getConfig(); + const content = await getDecoratedConfig(); if (!content) { throw new Error('No config content loaded'); @@ -43,9 +47,9 @@ async function init() { canaryUpdates = true; } - const feedURL = buildFeedUrl(canaryUpdates); + const feedURL = buildFeedUrl(canaryUpdates, version); - autoUpdater.setFeedURL(`${feedURL}/${version}`); + autoUpdater.setFeedURL(feedURL); setTimeout(() => { autoUpdater.checkForUpdates(); @@ -65,11 +69,14 @@ module.exports = win => { const {rpc} = win; - const onupdate = (ev, releaseNotes, releaseName) => { - rpc.emit('update available', {releaseNotes, releaseName}); + const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => { + const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`; + rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall}); }; - autoUpdater.on('update-downloaded', onupdate); + const eventName = isLinux ? 'update-available' : 'update-downloaded'; + + autoUpdater.on(eventName, onupdate); rpc.once('quit and install', () => { autoUpdater.quitAndInstall(); @@ -80,9 +87,9 @@ module.exports = win => { const newUpdateIsCanary = isCanary(updateChannel); if (newUpdateIsCanary !== canaryUpdates) { - const feedURL = buildFeedUrl(newUpdateIsCanary); + const feedURL = buildFeedUrl(newUpdateIsCanary, version); - autoUpdater.setFeedURL(`${feedURL}/${version}`); + autoUpdater.setFeedURL(feedURL); autoUpdater.checkForUpdates(); canaryUpdates = newUpdateIsCanary; @@ -90,6 +97,6 @@ module.exports = win => { }); win.on('close', () => { - autoUpdater.removeListener('update-downloaded', onupdate); + autoUpdater.removeListener(eventName, onupdate); }); }; diff --git a/lib/actions/updater.js b/lib/actions/updater.js index 1c55eaad..f4d26389 100644 --- a/lib/actions/updater.js +++ b/lib/actions/updater.js @@ -10,10 +10,12 @@ export function installUpdate() { }; } -export function updateAvailable(version, notes) { +export function updateAvailable(version, notes, releaseUrl, canInstall) { return { type: UPDATE_AVAILABLE, version, - notes + notes, + releaseUrl, + canInstall }; } diff --git a/lib/components/notifications.js b/lib/components/notifications.js index c9189721..2192cf16 100644 --- a/lib/components/notifications.js +++ b/lib/components/notifications.js @@ -79,20 +79,38 @@ export default class Notifications extends PureComponent { window.require('electron').shell.openExternal(ev.target.href); ev.preventDefault(); }} - href={`https://github.com/zeit/hyper/releases/tag/${this.props.updateVersion}`} + href={this.props.updateReleaseUrl} > notes ).{' '} - - Restart - .{' '} + {this.props.updateCanInstall ? ( + + Restart + + ) : ( + { + window.require('electron').shell.openExternal(ev.target.href); + ev.preventDefault(); + }} + href={this.props.updateReleaseUrl} + > + Download + + )}.{' '} )} {this.props.customChildren} diff --git a/lib/containers/notifications.js b/lib/containers/notifications.js index f761d611..202ba131 100644 --- a/lib/containers/notifications.js +++ b/lib/containers/notifications.js @@ -34,7 +34,9 @@ const NotificationsContainer = connect( Object.assign(state_, { updateShowing: true, updateVersion: ui.updateVersion, - updateNote: ui.updateNotes.split('\n')[0] + updateNote: ui.updateNotes.split('\n')[0], + updateReleaseUrl: ui.updateReleaseUrl, + updateCanInstall: ui.updateCanInstall }); } else if (notifications.message) { Object.assign(state_, { diff --git a/lib/index.js b/lib/index.js index b36ad816..b2ee32c7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -147,8 +147,8 @@ rpc.on('open file', ({path}) => { store_.dispatch(uiActions.openFile(path)); }); -rpc.on('update available', ({releaseName, releaseNotes}) => { - store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes)); +rpc.on('update available', ({releaseName, releaseNotes, releaseUrl, canInstall}) => { + store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes, releaseUrl, canInstall)); }); rpc.on('move', () => { diff --git a/lib/reducers/ui.js b/lib/reducers/ui.js index 99e11b34..55f27beb 100644 --- a/lib/reducers/ui.js +++ b/lib/reducers/ui.js @@ -346,7 +346,9 @@ const reducer = (state = initial, action) => { case UPDATE_AVAILABLE: state_ = state.merge({ updateVersion: action.version, - updateNotes: action.notes || '' + updateNotes: action.notes || '', + updateReleaseUrl: action.releaseUrl, + updateCanInstall: !!action.canInstall }); break; }