From 73a51db7cf77fdd2bf3d2d8c1809dc78de78b837 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sun, 25 Oct 2015 22:26:33 +0300 Subject: [PATCH 01/14] no auto-update (WIP) --- app/scripts/app.js | 4 +- app/scripts/comp/launcher.js | 22 ++++++++ app/scripts/comp/updater.js | 50 +++++++++++++++++++ app/scripts/models/app-settings-model.js | 3 +- .../views/settings/settings-general-view.js | 37 ++++++++++++-- app/templates/settings/settings-general.html | 10 +++- electron/loading.html | 32 ++++++++++++ electron/main.js | 50 ++++++++++++++++--- 8 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 app/scripts/comp/updater.js create mode 100644 electron/loading.html diff --git a/app/scripts/app.js b/app/scripts/app.js index d013e834..b902c7da 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -4,11 +4,13 @@ var AppModel = require('./models/app-model'), AppView = require('./views/app-view'), KeyHandler = require('./comp/key-handler'), Alerts = require('./comp/alerts'), - DropboxLink = require('./comp/dropbox-link'); + DropboxLink = require('./comp/dropbox-link'), + Updater = require('./comp/updater'); $(function() { require('./mixins/view'); + Updater.check(); if (location.href.indexOf('state=') >= 0) { DropboxLink.receive(); return; diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index bc9f3df3..504c8334 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -4,6 +4,7 @@ var Backbone = require('backbone'); var Launcher; if (window.process && window.process.versions && window.process.versions.electron) { + /* jshint node:true */ Launcher = { name: 'electron', version: window.process.versions.electron, @@ -35,6 +36,27 @@ if (window.process && window.process.versions && window.process.versions.electro }, fileExists: function(path) { return this.req('fs').existsSync(path); + }, + httpGet: function(config) { + var http = require(config.url.lastIndexOf('https', 0) === 0 ? 'https' : 'http'); + http.get(config.url, function(res) { + var data = []; + res.on('data', function (chunk) { data.push(chunk); }); + res.on('end', function() { + console.log('data', data); + data = Buffer.concat(data); + console.log('data', data); + if (config.utf8) { + data = data.toString('utf8'); + } + console.log('data', data); + if (config.complete) { + config.copmlete(null, data); + } + }); + }).on('error', function(err) { + if (config.complete) { config.complete(err); } + }); } }; window.launcherOpen = function(path) { diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js new file mode 100644 index 00000000..917c0854 --- /dev/null +++ b/app/scripts/comp/updater.js @@ -0,0 +1,50 @@ +'use strict'; + +var RuntimeInfo = require('./runtime-info'), + Links = require('../const/links'), + Launcher = require('../comp/launcher'); + +var Updater = { + lastCheckDate: null, + lastVersion: null, + lastVersionReleaseDate: null, + needUpdate: null, + status: 'ready', + check: function(complete) { + if (!Launcher) { + return; + } + this.status = 'checking'; + Launcher.httpGet({ + url: Links.WebApp + 'manifest.appcache', + utf8: true, + complete: (function (err, data) { + if (err) { + this.status = 'err'; + if (complete) { + complete(err); + } + return; + } + var match = data.match('#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)'); + if (!match) { + this.status = 'err'; + if (complete) { + complete(err); + } + return; + } + this.lastVersionReleaseDate = new Date(match[1]); + this.lastVersion = match[2]; + this.lastCheckDate = new Date(); + this.status = 'ok'; + this.needUpdate = this.lastVersion === RuntimeInfo.version; + if (complete) { + complete(); + } + }).bind(this) + }); + } +}; + +module.exports = Updater; diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 2afd7692..4d80f6d0 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -5,7 +5,8 @@ var Backbone = require('backbone'); var AppSettingsModel = Backbone.Model.extend({ defaults: { theme: 'd', - lastOpenFile: '' + lastOpenFile: '', + autoUpdate: true }, initialize: function() { diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index c2bcd8d5..33790a5e 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -2,13 +2,16 @@ var Backbone = require('backbone'), Launcher = require('../../comp/launcher'), + Updater = require('../../comp/updater'), + Format = require('../../util/format'), AppSettingsModel = require('../../models/app-settings-model'); var SettingsGeneralView = Backbone.View.extend({ template: require('templates/settings/settings-general.html'), events: { - 'change .settings__general-theme': 'changeTheme', + 'change #settings__general-theme': 'changeTheme', + 'change #settings__general-auto-update': 'changeAutoUpdate', 'click .settings__general-dev-tools-link': 'openDevTools' }, @@ -19,10 +22,30 @@ var SettingsGeneralView = Backbone.View.extend({ }, render: function() { - var activeTheme = AppSettingsModel.instance.get('theme'); + var lastUpdateCheck; + switch (Updater.status) { + case 'checking': + lastUpdateCheck = 'Checking...'; + break; + case 'err': + lastUpdateCheck = 'Error checking'; + break; + case 'ok': + lastUpdateCheck = Format.dtStr(Updater.lastCheckDate) + ': ' + + (Updater.needUpdate ? 'New version available: ' + Updater.lastVersion + + ' (released ' + Format.dStr(Updater.lastVersionReleaseDate) + ')' + : 'You are using the latest version'); + break; + default: + lastUpdateCheck = 'Never'; + break; + } this.renderTemplate({ themes: this.allThemes, - activeTheme: activeTheme, + activeTheme: AppSettingsModel.instance.get('theme'), + autoUpdate: AppSettingsModel.instance.get('autoUpdate'), + canAutoUpdate: !!Launcher, + lastUpdateCheck: lastUpdateCheck, devTools: Launcher && Launcher.devTools }); }, @@ -32,6 +55,14 @@ var SettingsGeneralView = Backbone.View.extend({ AppSettingsModel.instance.set('theme', theme); }, + changeAutoUpdate: function(e) { + var autoUpdate = e.target.checked; + AppSettingsModel.instance.set('autoUpdate', autoUpdate); + if (autoUpdate) { + Updater.check(); + } + }, + openDevTools: function() { if (Launcher) { Launcher.openDevTools(); diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index eeea06cc..0e64725b 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -3,12 +3,20 @@

Appearance

- <% _.forEach(themes, function(name, key) { %> <% }); %>
+ <% if (canAutoUpdate) { %> +

Function

+
+ /> + +
Last update check: <%- lastUpdateCheck %>
+
+ <% } %> <% if (devTools) { %>

Advanced

Show dev tools diff --git a/electron/loading.html b/electron/loading.html new file mode 100644 index 00000000..3ce680e5 --- /dev/null +++ b/electron/loading.html @@ -0,0 +1,32 @@ + + + + + KeeWeb + + + + +

Loading... Please wait

+ + diff --git a/electron/main.js b/electron/main.js index a15c55c1..55e903a8 100644 --- a/electron/main.js +++ b/electron/main.js @@ -10,12 +10,13 @@ var app = require('app'), var mainWindow = null, openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0], - ready = false; + ready = false, + htmlPath = path.join(app.getPath('userData'), 'index.html'); + +htmlPath = path.join(__dirname, '../tmp/index.html'); app.on('window-all-closed', function() { app.quit(); }); app.on('ready', function() { - var htmlPath = path.join(app.getPath('userData'), 'index.html'); - mainWindow = new BrowserWindow({ show: false, width: 1000, height: 700, 'min-width': 600, 'min-height': 300, @@ -25,12 +26,14 @@ app.on('ready', function() { if (fs.existsSync(htmlPath)) { mainWindow.loadUrl('file://' + htmlPath); } else { - mainWindow.loadUrl('https://antelle.github.io/keeweb/index.html'); + downloadFile(); } mainWindow.webContents.on('dom-ready', function() { - mainWindow.show(); - ready = true; - notifyOpenFile(); + setTimeout(function() { + mainWindow.show(); + ready = true; + notifyOpenFile(); + }, 50); }); mainWindow.on('closed', function() { mainWindow = null; }); }); @@ -48,3 +51,36 @@ function notifyOpenFile() { openFile = null; } } + +function downloadFile() { + console.log('Downloading file...'); + mainWindow.loadUrl('file://' + path.join(__dirname, 'loading.html')); + var fileData = []; + require('https').get('https://antelle.github.io/keeweb/index.html', function(res) { + res.on('data', function (chunk) { + fileData.push(chunk); + }); + res.on('end', function() { + fileData = Buffer.concat(fileData); + var fileDataStr = fileData.toString('utf8'); + if (/^\s*\s*$/.test(fileDataStr) && fileData.byteLength > 100000) { + fs.writeFileSync(htmlPath, fileData); + if (mainWindow) { + mainWindow.loadUrl('file://' + htmlPath); + } + } else { + showDownloadError('Invalid file downloaded'); + } + }); + }).on('error', function(err) { + showDownloadError(err); + }); +} + +function showDownloadError(err) { + console.error(err); + if (mainWindow) { + mainWindow.webContents.executeJavaScript('setTitle("Failed to download the app. Please restart me.
' + + 'This app requires Internet connection to start for the first time.")'); + } +} From b0a9cc90dfd6b88e7019da9ddb4d673e72ee33f6 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 30 Oct 2015 00:20:01 +0300 Subject: [PATCH 02/14] Updater implementation (WIP) --- app/scripts/app.js | 3 +- app/scripts/comp/launcher.js | 25 +--- app/scripts/comp/updater.js | 131 +++++++++++++----- app/scripts/models/update-model.js | 44 ++++++ app/scripts/views/app-view.js | 9 ++ .../views/settings/settings-general-view.js | 70 +++++++--- app/templates/settings/settings-general.html | 2 +- 7 files changed, 206 insertions(+), 78 deletions(-) create mode 100644 app/scripts/models/update-model.js diff --git a/app/scripts/app.js b/app/scripts/app.js index b902c7da..89aee1e9 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -10,7 +10,6 @@ var AppModel = require('./models/app-model'), $(function() { require('./mixins/view'); - Updater.check(); if (location.href.indexOf('state=') >= 0) { DropboxLink.receive(); return; @@ -35,6 +34,6 @@ $(function() { function showApp() { var appModel = new AppModel(); new AppView({ model: appModel }).render().showOpenFile(appModel.settings.get('lastOpenFile')); + Updater.init(); } }); - diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index 504c8334..35117e93 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -28,6 +28,10 @@ if (window.process && window.process.versions && window.process.versions.electro filters: [{ name: 'KeePass files', extensions: ['kdbx'] }] }, cb); }, + writeAppFile: function(data) { + var path = this.req('path').join(this.req('remote').require('app').getPath('userData'), 'index.html'); + this.writeFile(path, data); + }, writeFile: function(path, data) { this.req('fs').writeFileSync(path, new window.Buffer(data)); }, @@ -36,27 +40,6 @@ if (window.process && window.process.versions && window.process.versions.electro }, fileExists: function(path) { return this.req('fs').existsSync(path); - }, - httpGet: function(config) { - var http = require(config.url.lastIndexOf('https', 0) === 0 ? 'https' : 'http'); - http.get(config.url, function(res) { - var data = []; - res.on('data', function (chunk) { data.push(chunk); }); - res.on('end', function() { - console.log('data', data); - data = Buffer.concat(data); - console.log('data', data); - if (config.utf8) { - data = data.toString('utf8'); - } - console.log('data', data); - if (config.complete) { - config.copmlete(null, data); - } - }); - }).on('error', function(err) { - if (config.complete) { config.complete(err); } - }); } }; window.launcherOpen = function(path) { diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index 917c0854..07acd532 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -2,48 +2,117 @@ var RuntimeInfo = require('./runtime-info'), Links = require('../const/links'), - Launcher = require('../comp/launcher'); + Launcher = require('../comp/launcher'), + AppSettingsModel = require('../models/app-settings-model'), + UpdateModel = require('../models/update-model'); var Updater = { - lastCheckDate: null, - lastVersion: null, - lastVersionReleaseDate: null, - needUpdate: null, - status: 'ready', - check: function(complete) { + UpdateInterval: 1000*60*60*24, + MinUpdateTimeout: 500, + MinUpdateSize: 100000, + nextCheckTimeout: null, + enabledAutoUpdate: function() { + return Launcher && AppSettingsModel.instance.get('autoUpdate'); + }, + init: function() { + var willCheckNow = this.scheduleNextCheck(); + if (!willCheckNow && this.enabledAutoUpdate()) { + this.update(); + } + }, + scheduleNextCheck: function() { + if (this.nextCheckTimeout) { + clearTimeout(this.nextCheckTimeout); + this.nextCheckTimeout = null; + } + if (!this.enabledAutoUpdate()) { + return; + } + var timeDiff = this.StartupUpdateInterval; + var lastCheckDate = UpdateModel.instance.get('lastCheckDate'); + if (lastCheckDate) { + timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval); + } + this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff); + return timeDiff === this.MinUpdateTimeout; + }, + check: function() { if (!Launcher) { return; } - this.status = 'checking'; - Launcher.httpGet({ + UpdateModel.instance.set('status', 'checking'); + var that = this; + // TODO: potential DDoS in case on any error! Introduce rate limiting here + $.ajax({ + type: 'GET', url: Links.WebApp + 'manifest.appcache', - utf8: true, - complete: (function (err, data) { - if (err) { - this.status = 'err'; - if (complete) { - complete(err); - } - return; - } - var match = data.match('#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)'); + dataType: 'text', + success: function (data) { + var dt = new Date(); + UpdateModel.instance.set('lastCheckDate', dt); + var match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/); if (!match) { - this.status = 'err'; - if (complete) { - complete(err); - } + var errMsg = 'No version info found'; + UpdateModel.instance.set('lastError', errMsg); + UpdateModel.instance.set('status', 'error'); + UpdateModel.instance.save(); + that.scheduleNextCheck(); return; } - this.lastVersionReleaseDate = new Date(match[1]); - this.lastVersion = match[2]; - this.lastCheckDate = new Date(); - this.status = 'ok'; - this.needUpdate = this.lastVersion === RuntimeInfo.version; - if (complete) { - complete(); + UpdateModel.instance.set('lastSuccessCheckDate', dt); + UpdateModel.instance.set('lastVersionReleaseDate', new Date(match[1])); + UpdateModel.instance.set('lastVersion', match[2]); + UpdateModel.instance.set('status', 'ok'); + UpdateModel.instance.save(); + that.scheduleNextCheck(); + if (that.enabledAutoUpdate()) { + that.update(); } - }).bind(this) + }, + error: function() { + UpdateModel.instance.set('lastCheckDate', new Date()); + UpdateModel.instance.set('lastError', 'Error downloading last version info'); + UpdateModel.instance.set('status', 'error'); + UpdateModel.instance.save(); + that.scheduleNextCheck(); + } }); + }, + update: function() { + if (!Launcher || + UpdateModel.instance.get('version') === RuntimeInfo.version || + UpdateModel.instance.get('updateStatus')) { + return; + } + // TODO: potential DDoS in case on any error! Save file with version and check before the download + UpdateModel.instance.set('updateStatus', 'downloading'); + var xhr = new XMLHttpRequest(); + xhr.addEventListener('load', (function(e) { + if (xhr.response.byteLength > this.MinUpdateSize) { + UpdateModel.instance.set('updateStatus', 'downloaded'); + try { + Launcher.writeAppFile(xhr.response); + } catch (e) { + console.error('Error writing updated file', e); + UpdateModel.instance.set('updateStatus', 'error'); + } + Backbone.trigger('update-app'); + } else { + console.error('Bad downloaded file size: ' + xhr.response.byteLength); + UpdateModel.instance.set('updateStatus', 'error'); + } + }).bind(this)); + xhr.addEventListener('error', updateFailed); + xhr.addEventListener('abort', updateFailed); + xhr.addEventListener('timeout', updateFailed); + xhr.open('GET', Links.WebApp); + xhr.responseType = 'arraybuffer'; + xhr.send(); + + function updateFailed(e) { + console.error('XHR error downloading update', e); + UpdateModel.instance.set('updateStatus', 'error'); + } } }; diff --git a/app/scripts/models/update-model.js b/app/scripts/models/update-model.js new file mode 100644 index 00000000..9f171c50 --- /dev/null +++ b/app/scripts/models/update-model.js @@ -0,0 +1,44 @@ +'use strict'; + +var Backbone = require('backbone'); + +var UpdateModel = Backbone.Model.extend({ + defaults: { + lastSuccessCheckDate: null, + lastCheckDate: null, + lastVersion: null, + lastVersionReleaseDate: null, + lastError: null, + status: null, + updateStatus: null, + lastRequestDate: null + }, + + initialize: function() { + }, + + load: function() { + if (localStorage.updateInfo) { + try { + var data = JSON.parse(localStorage.updateInfo); + _.each(data, function(val, key) { + if (/Date$/.test(key)) { + data[key] = val ? new Date(val) : null + } + }); + this.set(data, { silent: true }); + } catch (e) { /* failed to load model */ } + } + }, + + save: function() { + var attr = _.clone(this.attributes); + delete attr.updateStatus; + localStorage.updateInfo = JSON.stringify(attr); + } +}); + +UpdateModel.instance = new UpdateModel(); +UpdateModel.instance.load(); + +module.exports = UpdateModel; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 85357a95..0787358b 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -53,6 +53,7 @@ var AppView = Backbone.View.extend({ this.listenTo(Backbone, 'toggle-menu', this.toggleMenu); this.listenTo(Backbone, 'toggle-details', this.toggleDetails); this.listenTo(Backbone, 'launcher-open-file', this.launcherOpenFile); + this.listenTo(Backbone, 'update-app', this.updateApp); window.onbeforeunload = this.beforeUnload.bind(this); window.onresize = this.windowResize.bind(this); @@ -96,6 +97,14 @@ var AppView = Backbone.View.extend({ } }, + updateApp: function() { + if (this.model.files.hasOpenFiles()) { + // TODO: show update bubble + } else { + this.location.reload(); + } + }, + showEntries: function() { this.views.menu.show(); this.views.menuDrag.show(); diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 33790a5e..44f20a3d 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -4,7 +4,9 @@ var Backbone = require('backbone'), Launcher = require('../../comp/launcher'), Updater = require('../../comp/updater'), Format = require('../../util/format'), - AppSettingsModel = require('../../models/app-settings-model'); + AppSettingsModel = require('../../models/app-settings-model'), + UpdateModel = require('../../models/update-model'), + RuntimeInfo = require('../../comp/runtime-info'); var SettingsGeneralView = Backbone.View.extend({ template: require('templates/settings/settings-general.html'), @@ -21,35 +23,57 @@ var SettingsGeneralView = Backbone.View.extend({ wh: 'white' }, + initialize: function() { + this.listenTo(UpdateModel.instance, 'change:status', this.render, this); + }, + render: function() { - var lastUpdateCheck; - switch (Updater.status) { - case 'checking': - lastUpdateCheck = 'Checking...'; - break; - case 'err': - lastUpdateCheck = 'Error checking'; - break; - case 'ok': - lastUpdateCheck = Format.dtStr(Updater.lastCheckDate) + ': ' + - (Updater.needUpdate ? 'New version available: ' + Updater.lastVersion + - ' (released ' + Format.dStr(Updater.lastVersionReleaseDate) + ')' - : 'You are using the latest version'); - break; - default: - lastUpdateCheck = 'Never'; - break; - } this.renderTemplate({ themes: this.allThemes, activeTheme: AppSettingsModel.instance.get('theme'), - autoUpdate: AppSettingsModel.instance.get('autoUpdate'), + devTools: Launcher && Launcher.devTools, canAutoUpdate: !!Launcher, - lastUpdateCheck: lastUpdateCheck, - devTools: Launcher && Launcher.devTools + autoUpdate: Updater.enabledAutoUpdate(), + updateInfo: this.getUpdateInfo() }); }, + getUpdateInfo: function() { + switch (UpdateModel.instance.get('updateStatus')) { + case 'downloading': + return 'Downloading update...'; + case 'downloaded': + return 'Downloaded new version'; + case 'error': + return 'Error downloading new version'; + } + switch (UpdateModel.instance.get('status')) { + case 'checking': + return 'Checking for updates...'; + case 'error': + var errMsg = 'Error checking for updates'; + if (UpdateModel.instance.get('lastError')) { + errMsg += ': ' + UpdateModel.instance.get('lastError'); + } + if (UpdateModel.instance.get('lastSuccessCheckDate')) { + errMsg += '. Last successful check was at ' + Format.dtStr(UpdateModel.instance.get('lastSuccessCheckDate')) + + ': the latest version was ' + UpdateModel.instance.get('lastVersion'); + } + return errMsg; + case 'ok': + var msg = 'Checked at ' + Format.dtStr(UpdateModel.instance.get('lastCheckDate')) + ': '; + if (RuntimeInfo.version === UpdateModel.instance.get('lastVersion')) { + msg += 'you are using the latest version'; + } else { + msg += 'new version ' + UpdateModel.instance.get('lastVersion') + ' available, released at ' + + Format.dStr(UpdateModel.instance.get('lastVersionReleaseDate')); + } + return msg; + default: + return 'Never checked for updates'; + } + }, + changeTheme: function(e) { var theme = e.target.value; AppSettingsModel.instance.set('theme', theme); @@ -59,7 +83,7 @@ var SettingsGeneralView = Backbone.View.extend({ var autoUpdate = e.target.checked; AppSettingsModel.instance.set('autoUpdate', autoUpdate); if (autoUpdate) { - Updater.check(); + Updater.scheduleNextCheck(); } }, diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index 0e64725b..9db7b734 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -14,7 +14,7 @@
/> -
Last update check: <%- lastUpdateCheck %>
+
<%- updateInfo %>
<% } %> <% if (devTools) { %> From bfb337b499374599725a48ab97d831553f344360 Mon Sep 17 00:00:00 2001 From: Antelle Date: Fri, 13 Nov 2015 22:56:22 +0300 Subject: [PATCH 03/14] first working updater implementation --- app/scripts/comp/launcher.js | 7 +- app/scripts/comp/transport.js | 60 ++++++++++++ app/scripts/comp/updater.js | 147 ++++++++++++++++++----------- app/scripts/const/links.js | 3 +- app/scripts/models/update-model.js | 5 +- electron/loading.html | 32 ------- electron/main.js | 39 +------- 7 files changed, 163 insertions(+), 130 deletions(-) create mode 100644 app/scripts/comp/transport.js delete mode 100644 electron/loading.html diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index 424d39fb..ecbd819b 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -30,13 +30,12 @@ if (window.process && window.process.versions && window.process.versions.electro filters: [{ name: 'KeePass files', extensions: ['kdbx'] }] }, cb); }, - writeAppFile: function(data) { - var path = this.req('path').join(this.req('remote').require('app').getPath('userData'), 'index.html'); - this.writeFile(path, data); - }, getUserDataPath: function(fileName) { return this.req('path').join(this.remReq('app').getPath('userData'), fileName || ''); }, + getTempPath: function(fileName) { + return this.req('path').join(this.remReq('app').getPath('temp'), fileName || ''); + }, writeFile: function(path, data) { this.req('fs').writeFileSync(path, new window.Buffer(data)); }, diff --git a/app/scripts/comp/transport.js b/app/scripts/comp/transport.js new file mode 100644 index 00000000..ef70d473 --- /dev/null +++ b/app/scripts/comp/transport.js @@ -0,0 +1,60 @@ +'use strict'; + +var Launcher = require('./launcher'); + +var Transport = { + httpGet: function(config) { + var tmpFile; + var fs = Launcher.req('fs'); + if (config.file) { + tmpFile = Launcher.getTempPath(config.file); + if (fs.existsSync(tmpFile)) { + try { + if (config.cache && fs.statSync(tmpFile).size > 0) { + console.log('File already downloaded ' + config.url); + return config.success(tmpFile); + } else { + fs.unlinkSync(tmpFile); + } + } catch (e) { + fs.unlink(tmpFile); + } + } + } + var proto = config.url.split(':')[0]; + console.log('GET ' + config.url); + Launcher.req(proto).get(config.url, function(res) { + console.log('Response from ' + config.url + ': ' + res.statusCode); + if (res.statusCode === 200) { + if (config.file) { + var file = fs.createWriteStream(tmpFile); + res.pipe(file); + file.on('finish', function() { + file.close(function() { config.success(tmpFile); }); + }); + file.on('error', function(err) { config.error(err); }); + } else { + var data = []; + res.on('data', function(chunk) { data.push(chunk); }); + res.on('end', function() { + data = window.Buffer.concat(data); + if (config.utf8) { + data = data.toString('utf8'); + } + config.success(data); + }); + } + } else { + config.error('HTTP status ' + res.statusCode); + } + }).on('error', function(e) { + console.error('Cannot GET ' + config.url, e); + if (tmpFile) { + fs.unlink(tmpFile); + } + config.error(e); + }); + } +}; + +module.exports = Transport; diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index c3ad8bd8..33b3308b 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -5,20 +5,23 @@ var Backbone = require('backbone'), Links = require('../const/links'), Launcher = require('../comp/launcher'), AppSettingsModel = require('../models/app-settings-model'), - UpdateModel = require('../models/update-model'); + UpdateModel = require('../models/update-model'), + Transport = require('../comp/transport'); var Updater = { UpdateInterval: 1000*60*60*24, - MinUpdateTimeout: 500, - MinUpdateSize: 100000, + MinUpdateTimeout: 500*10, + MinUpdateSize: 10000, + UpdateCheckFiles: ['index.html', 'app.js'], nextCheckTimeout: null, + updateCheckDate: new Date(0), enabledAutoUpdate: function() { return Launcher && AppSettingsModel.instance.get('autoUpdate'); }, init: function() { var willCheckNow = this.scheduleNextCheck(); if (!willCheckNow && this.enabledAutoUpdate()) { - this.update(); + this.check(); } }, scheduleNextCheck: function() { @@ -29,91 +32,125 @@ var Updater = { if (!this.enabledAutoUpdate()) { return; } - var timeDiff = this.StartupUpdateInterval; + var timeDiff = this.MinUpdateTimeout; var lastCheckDate = UpdateModel.instance.get('lastCheckDate'); if (lastCheckDate) { timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval); } this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff); + console.log('Update check will happen in ' + Math.round(timeDiff / 1000) + ' s'); return timeDiff === this.MinUpdateTimeout; }, - check: function() { + check: function(startedByUser) { if (!Launcher) { return; } UpdateModel.instance.set('status', 'checking'); var that = this; - // TODO: potential DDoS in case on any error! Introduce rate limiting here - $.ajax({ - type: 'GET', + if (!startedByUser) { + // additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour + var diffMs = new Date() - this.updateCheckDate; + if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) { + console.error('Prevented update check; last check was performed at ' + this.updateCheckDate); + that.scheduleNextCheck(); + return; + } + this.updateCheckDate = new Date(); + } + console.log('Checking for update...'); + Transport.httpGet({ url: Links.WebApp + 'manifest.appcache', - dataType: 'text', - success: function (data) { + utf8: true, + success: function(data) { var dt = new Date(); - UpdateModel.instance.set('lastCheckDate', dt); var match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/); + console.log('Update check: ' + (match ? match[0] : 'unknown')); if (!match) { var errMsg = 'No version info found'; - UpdateModel.instance.set('lastError', errMsg); - UpdateModel.instance.set('status', 'error'); + UpdateModel.instance.set({ status: 'error', lastCheckDate: dt, lastCheckError: errMsg }); UpdateModel.instance.save(); that.scheduleNextCheck(); return; } - UpdateModel.instance.set('lastSuccessCheckDate', dt); - UpdateModel.instance.set('lastVersionReleaseDate', new Date(match[1])); - UpdateModel.instance.set('lastVersion', match[2]); - UpdateModel.instance.set('status', 'ok'); + UpdateModel.instance.set({ + status: 'ok', + lastCheckDate: dt, + lastSuccessCheckDate: dt, + lastVersionReleaseDate: new Date(match[1]), + lastVersion: match[2], + lastcheckError: null + }); UpdateModel.instance.save(); that.scheduleNextCheck(); - if (that.enabledAutoUpdate()) { - that.update(); - } + that.update(startedByUser); }, - error: function() { - UpdateModel.instance.set('lastCheckDate', new Date()); - UpdateModel.instance.set('lastError', 'Error downloading last version info'); - UpdateModel.instance.set('status', 'error'); + error: function(e) { + console.error('Update check error', e); + UpdateModel.instance.set({ + status: 'error', + lastCheckDate: new Date(), + lastCheckError: 'Error checking last version' + }); UpdateModel.instance.save(); that.scheduleNextCheck(); } }); }, - update: function() { - if (!Launcher || - UpdateModel.instance.get('version') === RuntimeInfo.version || - UpdateModel.instance.get('updateStatus')) { + update: function(startedByUser) { + var ver = UpdateModel.instance.get('lastVersion'); + if (!Launcher || ver === RuntimeInfo.version || UpdateModel.instance.get('updateStatus')) { + console.log('You are using the latest version'); return; } - // TODO: potential DDoS in case on any error! Save file with version and check before the download - UpdateModel.instance.set('updateStatus', 'downloading'); - var xhr = new XMLHttpRequest(); - xhr.addEventListener('load', (function() { - if (xhr.response.byteLength > this.MinUpdateSize) { + UpdateModel.instance.set({ updateStatus: 'downloading', updateError: null }); + var that = this; + console.log('Downloading update', ver); + Transport.httpGet({ + url: Links.UpdateDesktop.replace('{ver}', ver), + file: 'KeeWeb-' + ver + '.zip', + cache: !startedByUser, + success: function(filePath) { UpdateModel.instance.set('updateStatus', 'downloaded'); - try { - Launcher.writeAppFile(xhr.response); - } catch (e) { - console.error('Error writing updated file', e); - UpdateModel.instance.set('updateStatus', 'error'); - } - Backbone.trigger('update-app'); - } else { - console.error('Bad downloaded file size: ' + xhr.response.byteLength); - UpdateModel.instance.set('updateStatus', 'error'); + console.error('Extracting update file', that.UpdateCheckFiles, filePath); + that.extractAppUpdate(filePath, function(err) { + if (err) { + console.error('Error extracting update', err); + UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error extracting update' }); + } else { + UpdateModel.instance.set({ updateStatus: 'ready', updateError: null }); + Backbone.trigger('update-app'); + } + }); + }, + error: function(e) { + console.error('Error downloading update', e); + UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error downloading update' }); } - }).bind(this)); - xhr.addEventListener('error', updateFailed); - xhr.addEventListener('abort', updateFailed); - xhr.addEventListener('timeout', updateFailed); - xhr.open('GET', Links.WebApp); - xhr.responseType = 'arraybuffer'; - xhr.send(); + }); + }, - function updateFailed(e) { - console.error('XHR error downloading update', e); - UpdateModel.instance.set('updateStatus', 'error'); - } + extractAppUpdate: function(updateFile, expectedFiles, cb) { + var appPath = Launcher.getUserDataPath(); + var StreamZip = Launcher.req('node-stream-zip'); + var zip = new StreamZip({ file: updateFile, storeEntries: true }); + zip.on('error', cb); + zip.on('ready', function() { + var containsAll = expectedFiles.every(function(expFile) { + var entry = zip.entry(expFile); + return entry && entry.isFile; + }); + if (!containsAll) { + return cb('Bad archive'); + } + zip.extract(null, appPath, function(err) { + zip.close(); + if (err) { + return cb(err); + } + Launcher.req('fs').unlink(updateFile); + cb(); + }); + }); } }; diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index 111ecb48..b48dae46 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -4,7 +4,8 @@ var Links = { Repo: 'https://github.com/antelle/keeweb', Desktop: 'https://github.com/antelle/keeweb/releases/latest', WebApp: 'https://antelle.github.io/keeweb/', - License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt' + License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt', + UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip' }; module.exports = Links; diff --git a/app/scripts/models/update-model.js b/app/scripts/models/update-model.js index baaad4b3..b0c4daf5 100644 --- a/app/scripts/models/update-model.js +++ b/app/scripts/models/update-model.js @@ -8,10 +8,10 @@ var UpdateModel = Backbone.Model.extend({ lastCheckDate: null, lastVersion: null, lastVersionReleaseDate: null, - lastError: null, + lastCheckError: null, status: null, updateStatus: null, - lastRequestDate: null + updateError: null }, initialize: function() { @@ -34,6 +34,7 @@ var UpdateModel = Backbone.Model.extend({ save: function() { var attr = _.clone(this.attributes); delete attr.updateStatus; + delete attr.updateError; localStorage.updateInfo = JSON.stringify(attr); } }); diff --git a/electron/loading.html b/electron/loading.html deleted file mode 100644 index 3ce680e5..00000000 --- a/electron/loading.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - KeeWeb - - - - -

Loading... Please wait

- - diff --git a/electron/main.js b/electron/main.js index e7642f22..89fde5f3 100644 --- a/electron/main.js +++ b/electron/main.js @@ -26,12 +26,12 @@ app.on('ready', function() { setMenu(); if (fs.existsSync(htmlPath)) { mainWindow.loadUrl('file://' + htmlPath); - } else { - downloadFile(); } + mainWindow.show(); // remove + mainWindow.openDevTools(); mainWindow.webContents.on('dom-ready', function() { setTimeout(function() { - mainWindow.show(); + //mainWindow.show(); ready = true; notifyOpenFile(); }, 50); @@ -88,36 +88,3 @@ function notifyOpenFile() { openFile = null; } } - -function downloadFile() { - console.log('Downloading file...'); - mainWindow.loadUrl('file://' + path.join(__dirname, 'loading.html')); - var fileData = []; - require('https').get('https://antelle.github.io/keeweb/index.html', function(res) { - res.on('data', function (chunk) { - fileData.push(chunk); - }); - res.on('end', function() { - fileData = Buffer.concat(fileData); - var fileDataStr = fileData.toString('utf8'); - if (/^\s*\s*$/.test(fileDataStr) && fileData.byteLength > 100000) { - fs.writeFileSync(htmlPath, fileData); - if (mainWindow) { - mainWindow.loadUrl('file://' + htmlPath); - } - } else { - showDownloadError('Invalid file downloaded'); - } - }); - }).on('error', function(err) { - showDownloadError(err); - }); -} - -function showDownloadError(err) { - console.error(err); - if (mainWindow) { - mainWindow.webContents.executeJavaScript('setTitle("Failed to download the app. Please restart me.
' + - 'This app requires Internet connection to start for the first time.")'); - } -} From 876148e9535ed5452ab4aa535bea7def38d331bf Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 10:46:53 +0300 Subject: [PATCH 04/14] split electron app to launcher, content and data --- electron/{main.js => app.js} | 17 ++++++++--------- electron/launcher.js | 31 +++++++++++++++++++++++++++++++ electron/package.json | 10 +++++++++- package.json | 9 +++++---- 4 files changed, 53 insertions(+), 14 deletions(-) rename electron/{main.js => app.js} (90%) create mode 100644 electron/launcher.js diff --git a/electron/main.js b/electron/app.js similarity index 90% rename from electron/main.js rename to electron/app.js index 89fde5f3..06c7a520 100644 --- a/electron/main.js +++ b/electron/app.js @@ -6,15 +6,18 @@ var app = require('app'), BrowserWindow = require('browser-window'), path = require('path'), - fs = require('fs'), Menu = require('menu'); var mainWindow = null, openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0], ready = false, - htmlPath = path.join(app.getPath('userData'), 'index.html'); + htmlPath = path.join(__dirname, 'index.html'); -htmlPath = path.join(__dirname, '../tmp/index.html'); +process.argv.forEach(function(arg) { + if (arg.lastIndexOf('--htmlpath=', 0) === 0) { + htmlPath = path.resolve(arg.replace('--htmlpath=', ''), 'index.html'); + } +}); app.on('window-all-closed', function() { app.quit(); }); app.on('ready', function() { @@ -24,14 +27,10 @@ app.on('ready', function() { icon: path.join(__dirname, 'icon.png') }); setMenu(); - if (fs.existsSync(htmlPath)) { - mainWindow.loadUrl('file://' + htmlPath); - } - mainWindow.show(); // remove - mainWindow.openDevTools(); + mainWindow.loadUrl('file://' + htmlPath); mainWindow.webContents.on('dom-ready', function() { setTimeout(function() { - //mainWindow.show(); + mainWindow.show(); ready = true; notifyOpenFile(); }, 50); diff --git a/electron/launcher.js b/electron/launcher.js new file mode 100644 index 00000000..e08dd5dd --- /dev/null +++ b/electron/launcher.js @@ -0,0 +1,31 @@ +'use strict'; + +/* jshint node:true */ +/* jshint browser:false */ + +var app = require('app'), + path = require('path'), + fs = require('fs'); + +var userDataDir = app.getPath('userData'), + appPathUserData = path.join(userDataDir, 'app.js'), + appPath = path.join(__dirname, 'app.js'); + +if (fs.existsSync(appPathUserData)) { + var versionLocal = require('./package.json').version; + try { + var versionUserData = require(path.join(userDataDir, 'package.json')).version; + versionLocal = versionLocal.split('.'); + versionUserData = versionUserData.split('.'); + for (var i = 0; i < versionLocal.length; i++) { + if (+versionUserData[i] > +versionLocal[i]) { + appPath = appPathUserData; + } + } + } + catch (e) { + console.error('Error reading user file version', e); + } +} + +require(appPath); diff --git a/electron/package.json b/electron/package.json index 32c4c597..4ad29bc7 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,5 +1,13 @@ { "name": "KeeWeb", "version": "0.2.1", - "main": "main.js" + "description": "KeePass web app", + "main": "launcher.js", + "repository": "https://github.com/antelle/keeweb", + "author": "Antelle", + "license": "MIT", + "readme": "../README.md", + "dependencies": { + "node-stream-zip": "^1.2.1" + } } diff --git a/package.json b/package.json index 8fd3fcef..f6df2908 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,14 @@ "string-replace-webpack-plugin": "0.0.2", "time-grunt": "^1.2.1", "uglify-loader": "^1.2.0", - "webpack": "^1.11.0", - "webpack-dev-server": "^1.10.1" + "webpack": "^1.11.0" }, "scripts": { "start": "grunt", - "test": "grunt test" + "test": "grunt test", + "postinstall": "npm install --prefix electron" }, "author": "Antelle", - "license": "MIT" + "license": "MIT", + "readme": "README.md" } From adcfe6a3df6ad00f2151a48ac714bf34c41ec654 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 10:56:45 +0300 Subject: [PATCH 05/14] fix entry point versioning --- electron/launcher.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/electron/launcher.js b/electron/launcher.js index e08dd5dd..84015d2d 100644 --- a/electron/launcher.js +++ b/electron/launcher.js @@ -1,3 +1,11 @@ +// KeeWeb launcher script + +// This script is distributed with the app and is its entry point +// It checks whether the app is available in userData folder and if its version is higher than local, launches it +// This script is the only part which will be updated only with the app itself, auto-update will not change it + +// (C) Antelle 2015, MIT license https://github.com/antelle/keeweb + 'use strict'; /* jshint node:true */ @@ -20,6 +28,10 @@ if (fs.existsSync(appPathUserData)) { for (var i = 0; i < versionLocal.length; i++) { if (+versionUserData[i] > +versionLocal[i]) { appPath = appPathUserData; + break; + } + if (+versionUserData[i] < +versionLocal[i]) { + break; } } } From a32fc8ef458498045b1d1f48f2be9a5de9fd0580 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 11:39:22 +0300 Subject: [PATCH 06/14] building index.html with app --- Gruntfile.js | 29 ++++++++++++++----- app/manifest.appcache | 2 +- app/scripts/comp/runtime-info.js | 1 + .../views/settings/settings-help-view.js | 2 +- package.json | 3 +- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index e5fa9e0c..b1ad0b7f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -76,6 +76,13 @@ module.exports = function(grunt) { expand: true, flatten: true }, + 'desktop_app_content': { + cwd: 'electron/', + src: '**', + dest: 'tmp/desktop/app/', + expand: true, + nonull: true + }, 'desktop_osx': { src: 'tmp/desktop/KeeWeb.dmg', dest: 'dist/desktop/KeeWeb.mac.dmg', @@ -141,12 +148,16 @@ module.exports = function(grunt) { 'string-replace': { manifest: { options: { - replacements: [{ - pattern: '# YYYY-MM-DD:v0.0.0', - replacement: '# ' + dt + ':v' + pkg.version - }] + replacements: [ + { pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version }, + { pattern: 'vElectron', replacement: electronVersion } + ] }, files: { 'dist/manifest.appcache': 'app/manifest.appcache' } + }, + 'desktop_html': { + options: { replacements: [{ pattern: ' manifest="manifest.appcache"', replacement: '' }] }, + files: { 'tmp/desktop/app/index.html': 'dist/index.html' } } }, webpack: { @@ -190,7 +201,8 @@ module.exports = function(grunt) { }]})}, { test: /runtime\-info\.js$/, loader: StringReplacePlugin.replace({ replacements: [ { pattern: /@@VERSION/g, replacement: function() { return pkg.version; } }, - { pattern: /@@DATE/g, replacement: function() { return dt; } } + { pattern: /@@DATE/g, replacement: function() { return dt; } }, + { pattern: /@@COMMIT/g, replacement: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); } } ]})}, { test: /zepto(\.min)?\.js$/, loader: 'exports?Zepto; delete window.$; delete window.Zepto;' }, { test: /baron(\.min)?\.js$/, loader: 'exports?baron; delete window.baron;' }, @@ -251,7 +263,7 @@ module.exports = function(grunt) { electron: { options: { name: 'KeeWeb', - dir: 'electron', + dir: 'tmp/desktop/app', out: 'tmp/desktop', version: electronVersion, overwrite: true, @@ -335,6 +347,7 @@ module.exports = function(grunt) { }); grunt.registerTask('default', [ + 'gitinfo', 'bower-install-simple', 'clean', 'jshint', @@ -347,13 +360,15 @@ module.exports = function(grunt) { 'postcss', 'inline', 'htmlmin', - 'string-replace' + 'string-replace:manifest' ]); grunt.registerTask('desktop', [ 'gitinfo', 'clean:desktop_tmp', 'clean:desktop_dist', + 'copy:desktop_app_content', + 'string-replace:desktop_html', 'electron', 'electron_builder', 'compress:linux', diff --git a/app/manifest.appcache b/app/manifest.appcache index 0f8b8573..2d741eb0 100644 --- a/app/manifest.appcache +++ b/app/manifest.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# YYYY-MM-DD:v0.0.0 +# YYYY-MM-DD:v0.0.0 evElectron CACHE: index.html diff --git a/app/scripts/comp/runtime-info.js b/app/scripts/comp/runtime-info.js index 880dc5dd..1255d5bd 100644 --- a/app/scripts/comp/runtime-info.js +++ b/app/scripts/comp/runtime-info.js @@ -5,6 +5,7 @@ var Launcher = require('../comp/launcher'); var RuntimeInfo = { version: '@@VERSION', buildDate: '@@DATE', + commit: '@@COMMIT', userAgent: navigator.userAgent, launcher: Launcher ? Launcher.name + ' v' + Launcher.version : '' }; diff --git a/app/scripts/views/settings/settings-help-view.js b/app/scripts/views/settings/settings-help-view.js index 04d00245..dac1efe0 100644 --- a/app/scripts/views/settings/settings-help-view.js +++ b/app/scripts/views/settings/settings-help-view.js @@ -8,7 +8,7 @@ var SettingsHelpView = Backbone.View.extend({ template: require('templates/settings/settings-help.html'), render: function() { - var appInfo = 'KeeWeb v' + RuntimeInfo.version + ' (built at ' + RuntimeInfo.buildDate + ')\n' + + var appInfo = 'KeeWeb v' + RuntimeInfo.version + ' (' + RuntimeInfo.commit + ', ' + RuntimeInfo.buildDate + ')\n' + 'Environment: ' + (RuntimeInfo.launcher ? RuntimeInfo.launcher : 'web') + '\n' + 'User-Agent: ' + RuntimeInfo.userAgent; this.renderTemplate({ diff --git a/package.json b/package.json index f6df2908..0b9b8bf7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "string-replace-webpack-plugin": "0.0.2", "time-grunt": "^1.2.1", "uglify-loader": "^1.2.0", - "webpack": "^1.11.0" + "webpack": "^1.11.0", + "webpack-dev-server": "^1.12.1" }, "scripts": { "start": "grunt", From 9ec30ff533fee790b528dc3d062d1ef4d07dcdcc Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 14:09:36 +0300 Subject: [PATCH 07/14] app restart on update --- Gruntfile.js | 17 +++++++--- app/scripts/comp/launcher.js | 21 ++++++++++++- app/scripts/comp/updater.js | 29 ++++++++++++----- app/scripts/const/links.js | 4 +-- app/scripts/models/app-settings-model.js | 5 ++- app/scripts/views/app-view.js | 29 +++++++++++------ .../views/settings/settings-general-view.js | 31 +++++++++++++------ app/styles/areas/_settings.scss | 10 ++++++ app/templates/settings/settings-general.html | 10 ++++++ electron/app.js | 19 ++++++++++-- electron/{launcher.js => main.js} | 0 electron/package.json | 2 +- 12 files changed, 138 insertions(+), 39 deletions(-) rename electron/{launcher.js => main.js} (100%) diff --git a/Gruntfile.js b/Gruntfile.js index b1ad0b7f..1603816e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,6 +3,9 @@ var fs = require('fs'), path = require('path'); +/* jshint node:true */ +/* jshint browser:false */ + var StringReplacePlugin = require('string-replace-webpack-plugin'); module.exports = function(grunt) { @@ -55,8 +58,8 @@ module.exports = function(grunt) { }, clean: { dist: ['dist', 'tmp'], - desktop_dist: ['dist/desktop'], - desktop_tmp: ['tmp/desktop'] + 'desktop_dist': ['dist/desktop'], + 'desktop_tmp': ['tmp/desktop'] }, copy: { html: { @@ -338,10 +341,12 @@ module.exports = function(grunt) { }, compress: { linux: { - options: { - archive: 'tmp/desktop/KeeWeb.linux.x64.zip' - }, + options: { archive: 'tmp/desktop/KeeWeb.linux.x64.zip' }, files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true }] + }, + 'desktop_update': { + options: { archive: 'dist/desktop/UpdateDesktop.zip' }, + files: [{ cwd: 'tmp/desktop/app', src: '**', expand: true }] } } }); @@ -364,11 +369,13 @@ module.exports = function(grunt) { ]); grunt.registerTask('desktop', [ + 'default', 'gitinfo', 'clean:desktop_tmp', 'clean:desktop_dist', 'copy:desktop_app_content', 'string-replace:desktop_html', + 'compress:desktop_update', 'electron', 'electron_builder', 'compress:linux', diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index ecbd819b..21b50fe3 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -46,9 +46,28 @@ if (window.process && window.process.versions && window.process.versions.electro fileExists: function(path) { return this.req('fs').existsSync(path); }, + preventExit: function(e) { + e.returnValue = false; + return false; + }, exit: function() { Launcher.exitRequested = true; - this.remReq('app').quit(); + this.requestExit(); + }, + requestExit: function() { + var app = this.remReq('app'); + if (this.restartPending) { + app.quitAndRestart(); + } else { + app.quit(); + } + }, + requestRestart: function() { + this.restartPending = true; + this.requestExit(); + }, + cancelRestart: function() { + this.restartPending = false; } }; window.launcherOpen = function(path) { diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index 33b3308b..e10b1c76 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -10,7 +10,7 @@ var Backbone = require('backbone'), var Updater = { UpdateInterval: 1000*60*60*24, - MinUpdateTimeout: 500*10, + MinUpdateTimeout: 500, MinUpdateSize: 10000, UpdateCheckFiles: ['index.html', 'app.js'], nextCheckTimeout: null, @@ -18,6 +18,10 @@ var Updater = { enabledAutoUpdate: function() { return Launcher && AppSettingsModel.instance.get('autoUpdate'); }, + updateInProgress: function() { + return UpdateModel.instance.get('status') === 'checking' || + ['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0; + }, init: function() { var willCheckNow = this.scheduleNextCheck(); if (!willCheckNow && this.enabledAutoUpdate()) { @@ -38,11 +42,11 @@ var Updater = { timeDiff = Math.min(Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout), this.UpdateInterval); } this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff); - console.log('Update check will happen in ' + Math.round(timeDiff / 1000) + ' s'); + console.log('Update check will happen in ' + Math.round(timeDiff / 1000) + 's'); return timeDiff === this.MinUpdateTimeout; }, check: function(startedByUser) { - if (!Launcher) { + if (!Launcher || this.updateInProgress()) { return; } UpdateModel.instance.set('status', 'checking'); @@ -72,6 +76,7 @@ var Updater = { that.scheduleNextCheck(); return; } + var prevLastVersion = UpdateModel.instance.get('lastVersion'); UpdateModel.instance.set({ status: 'ok', lastCheckDate: dt, @@ -82,6 +87,11 @@ var Updater = { }); UpdateModel.instance.save(); that.scheduleNextCheck(); + if (prevLastVersion === UpdateModel.instance.get('lastVersion') && + UpdateModel.instance.get('updateStatus') === 'ready') { + console.log('Waiting for the user to apply downloaded update'); + return; + } that.update(startedByUser); }, error: function(e) { @@ -98,7 +108,7 @@ var Updater = { }, update: function(startedByUser) { var ver = UpdateModel.instance.get('lastVersion'); - if (!Launcher || ver === RuntimeInfo.version || UpdateModel.instance.get('updateStatus')) { + if (!Launcher || ver === RuntimeInfo.version) { console.log('You are using the latest version'); return; } @@ -110,15 +120,17 @@ var Updater = { file: 'KeeWeb-' + ver + '.zip', cache: !startedByUser, success: function(filePath) { - UpdateModel.instance.set('updateStatus', 'downloaded'); - console.error('Extracting update file', that.UpdateCheckFiles, filePath); + UpdateModel.instance.set('updateStatus', 'extracting'); + console.log('Extracting update file', that.UpdateCheckFiles, filePath); that.extractAppUpdate(filePath, function(err) { if (err) { console.error('Error extracting update', err); UpdateModel.instance.set({ updateStatus: 'error', updateError: 'Error extracting update' }); } else { UpdateModel.instance.set({ updateStatus: 'ready', updateError: null }); - Backbone.trigger('update-app'); + if (!startedByUser) { + Backbone.trigger('update-app'); + } } }); }, @@ -129,7 +141,8 @@ var Updater = { }); }, - extractAppUpdate: function(updateFile, expectedFiles, cb) { + extractAppUpdate: function(updateFile, cb) { + var expectedFiles = this.UpdateCheckFiles; var appPath = Launcher.getUserDataPath(); var StreamZip = Launcher.req('node-stream-zip'); var zip = new StreamZip({ file: updateFile, storeEntries: true }); diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index b48dae46..aa93d4a4 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -3,9 +3,9 @@ var Links = { Repo: 'https://github.com/antelle/keeweb', Desktop: 'https://github.com/antelle/keeweb/releases/latest', - WebApp: 'https://antelle.github.io/keeweb/', + WebApp: 'http://localhost:8088/', License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt', - UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip' + UpdateDesktop: 'http://localhost:8088/releases/download/{ver}/UpdateDesktop.zip' }; module.exports = Links; diff --git a/app/scripts/models/app-settings-model.js b/app/scripts/models/app-settings-model.js index 4ed80fa6..820c6497 100644 --- a/app/scripts/models/app-settings-model.js +++ b/app/scripts/models/app-settings-model.js @@ -22,7 +22,10 @@ var AppSettingsModel = Backbone.Model.extend({ try { var data; if (Launcher) { - data = JSON.parse(Launcher.readFile(Launcher.getUserDataPath(FileName), 'utf8')); + var settingsFile = Launcher.getUserDataPath(FileName); + if (Launcher.fileExists(settingsFile)) { + data = JSON.parse(Launcher.readFile(settingsFile, 'utf8')); + } } else if (typeof localStorage !== 'undefined' && localStorage.appSettings) { data = JSON.parse(localStorage.appSettings); } diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index f95d9830..14ec5937 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -198,16 +198,25 @@ var AppView = Backbone.View.extend({ beforeUnload: function(e) { if (this.model.files.hasUnsavedFiles()) { if (Launcher && !Launcher.exitRequested) { - Alerts.yesno({ - header: 'Unsaved changes!', - body: 'You have unsaved files, all changes will be lost.', - buttons: [{result: 'yes', title: 'Exit and discard unsaved changes'}, {result: '', title: 'Don\'t exit'}], - success: function() { - Launcher.exit(); - } - }); - e.returnValue = false; - return false; + if (!this.exitAlertShown) { + var that = this; + that.exitAlertShown = true; + Alerts.yesno({ + header: 'Unsaved changes!', + body: 'You have unsaved files, all changes will be lost.', + buttons: [{result: 'yes', title: 'Exit and discard unsaved changes'}, {result: '', title: 'Don\'t exit'}], + success: function () { + Launcher.exit(); + }, + cancel: function() { + Launcher.cancelRestart(false); + }, + complete: function () { + that.exitAlertShown = false; + } + }); + } + return Launcher.preventExit(e); } return 'You have unsaved files, all changes will be lost.'; } diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 15995ee5..2282084b 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -15,6 +15,8 @@ var SettingsGeneralView = Backbone.View.extend({ 'change .settings__general-theme': 'changeTheme', 'change .settings__general-expand': 'changeExpandGroups', 'change .settings__general-auto-update': 'changeAutoUpdate', + 'click .settings__general-update-btn': 'checkUpdate', + 'click .settings__general-restart-btn': 'restartApp', 'click .settings__general-dev-tools-link': 'openDevTools' }, @@ -26,6 +28,7 @@ var SettingsGeneralView = Backbone.View.extend({ initialize: function() { this.listenTo(UpdateModel.instance, 'change:status', this.render, this); + this.listenTo(UpdateModel.instance, 'change:updateStatus', this.render, this); }, render: function() { @@ -36,19 +39,13 @@ var SettingsGeneralView = Backbone.View.extend({ devTools: Launcher && Launcher.devTools, canAutoUpdate: !!Launcher, autoUpdate: Updater.enabledAutoUpdate(), - updateInfo: this.getUpdateInfo() + updateInProgress: Updater.updateInProgress(), + updateInfo: this.getUpdateInfo(), + updateReady: UpdateModel.instance.get('updateStatus') === 'ready' }); }, getUpdateInfo: function() { - switch (UpdateModel.instance.get('updateStatus')) { - case 'downloading': - return 'Downloading update...'; - case 'downloaded': - return 'Downloaded new version'; - case 'error': - return 'Error downloading new version'; - } switch (UpdateModel.instance.get('status')) { case 'checking': return 'Checking for updates...'; @@ -70,6 +67,14 @@ var SettingsGeneralView = Backbone.View.extend({ msg += 'new version ' + UpdateModel.instance.get('lastVersion') + ' available, released at ' + Format.dStr(UpdateModel.instance.get('lastVersionReleaseDate')); } + switch (UpdateModel.instance.get('updateStatus')) { + case 'downloading': + return msg + '. Downloading update...'; + case 'extracting': + return msg + '. Extracting update...'; + case 'error': + return msg + '. There was an error downloading new version'; + } return msg; default: return 'Never checked for updates'; @@ -89,6 +94,14 @@ var SettingsGeneralView = Backbone.View.extend({ } }, + checkUpdate: function() { + Updater.check(true); + }, + + restartApp: function() { + Launcher.requestRestart(); + }, + changeExpandGroups: function(e) { var expand = e.target.checked; AppSettingsModel.instance.set('expandGroups', expand); diff --git a/app/styles/areas/_settings.scss b/app/styles/areas/_settings.scss index ea15d689..2f6c0789 100644 --- a/app/styles/areas/_settings.scss +++ b/app/styles/areas/_settings.scss @@ -71,4 +71,14 @@ float: right; display: none; } + + &__general-update-buttons { + margin-top: $base-spacing; + } + &__general-update-btn { + width: 15em; + } + &__general-restart-btn { + margin-left: $small-spacing; + } } diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index ec6a7345..af1b1bf1 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -20,6 +20,16 @@
<%- updateInfo %>
+
+ <% if (updateInProgress) { %> + + <% } else { %> + + <% } %> + <% if (updateReady) { %> + + <% } %> +
<% } %> <% if (devTools) { %>

Advanced

diff --git a/electron/app.js b/electron/app.js index 06c7a520..38be4f8b 100644 --- a/electron/app.js +++ b/electron/app.js @@ -11,6 +11,7 @@ var app = require('app'), var mainWindow = null, openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0], ready = false, + restartPending = false, htmlPath = path.join(__dirname, 'index.html'); process.argv.forEach(function(arg) { @@ -19,7 +20,9 @@ process.argv.forEach(function(arg) { } }); -app.on('window-all-closed', function() { app.quit(); }); +app.on('window-all-closed', function() { + app.quit(); +}); app.on('ready', function() { mainWindow = new BrowserWindow({ show: false, @@ -35,13 +38,25 @@ app.on('ready', function() { notifyOpenFile(); }, 50); }); - mainWindow.on('closed', function() { mainWindow = null; }); + mainWindow.on('closed', function() { + mainWindow = null; + }); }); app.on('open-file', function(e, path) { e.preventDefault(); openFile = path; notifyOpenFile(); }); +app.on('quit', function() { + if (restartPending) { + require('child_process').exec(process.execPath); + } +}); +app.quitAndRestart = function() { + restartPending = true; + app.quit(); + setTimeout(function() { restartPending = false; }, 1000); +}; function setMenu() { if (process.platform === 'darwin') { diff --git a/electron/launcher.js b/electron/main.js similarity index 100% rename from electron/launcher.js rename to electron/main.js diff --git a/electron/package.json b/electron/package.json index 4ad29bc7..e5e7b1e9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -2,7 +2,7 @@ "name": "KeeWeb", "version": "0.2.1", "description": "KeePass web app", - "main": "launcher.js", + "main": "main.js", "repository": "https://github.com/antelle/keeweb", "author": "Antelle", "license": "MIT", From 608f098f59e64584b0bc971fe0da18b6b4f3398b Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 18:28:36 +0300 Subject: [PATCH 08/14] Update alert --- app/scripts/comp/launcher.js | 3 +++ app/scripts/comp/updater.js | 27 +++++++++++++++++++ app/scripts/const/links.js | 4 +-- app/scripts/models/update-model.js | 10 ++++--- app/scripts/views/app-view.js | 4 +-- app/scripts/views/footer-view.js | 10 +++++-- .../views/settings/settings-general-view.js | 17 +++++++++--- app/styles/areas/_footer.scss | 5 ++++ app/styles/areas/_settings.scss | 4 +-- app/styles/base/_colors.scss | 5 ++-- app/styles/common/_fx.scss | 8 ++++++ app/templates/footer.html | 8 +++++- app/templates/settings/settings-general.html | 16 ++++++++++- 13 files changed, 100 insertions(+), 21 deletions(-) diff --git a/app/scripts/comp/launcher.js b/app/scripts/comp/launcher.js index 21b50fe3..239eed6e 100644 --- a/app/scripts/comp/launcher.js +++ b/app/scripts/comp/launcher.js @@ -19,6 +19,9 @@ if (window.process && window.process.versions && window.process.versions.electro openDevTools: function() { this.req('remote').getCurrentWindow().openDevTools(); }, + getAppVersion: function() { + return this.remReq('app').getVersion(); + }, getSaveFileName: function(defaultPath, cb) { if (defaultPath) { var homePath = this.remReq('app').getPath('userDesktop'); diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index e10b1c76..45f45030 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -15,19 +15,27 @@ var Updater = { UpdateCheckFiles: ['index.html', 'app.js'], nextCheckTimeout: null, updateCheckDate: new Date(0), + enabledAutoUpdate: function() { return Launcher && AppSettingsModel.instance.get('autoUpdate'); }, + updateInProgress: function() { return UpdateModel.instance.get('status') === 'checking' || ['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0; }, + init: function() { var willCheckNow = this.scheduleNextCheck(); if (!willCheckNow && this.enabledAutoUpdate()) { this.check(); } + if (!Launcher && window.applicationCache) { + window.applicationCache.addEventListener('updateready', this.checkAppCacheUpdateReady.bind(this)); + this.checkAppCacheUpdateReady(); + } }, + scheduleNextCheck: function() { if (this.nextCheckTimeout) { clearTimeout(this.nextCheckTimeout); @@ -45,10 +53,14 @@ var Updater = { console.log('Update check will happen in ' + Math.round(timeDiff / 1000) + 's'); return timeDiff === this.MinUpdateTimeout; }, + check: function(startedByUser) { if (!Launcher || this.updateInProgress()) { return; } + if (this.checkManualDownload()) { + return; + } UpdateModel.instance.set('status', 'checking'); var that = this; if (!startedByUser) { @@ -106,6 +118,13 @@ var Updater = { } }); }, + + checkManualDownload: function() { + if (+Launcher.getAppVersion().split('.')[1] <= 2) { + UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true }); + } + }, + update: function(startedByUser) { var ver = UpdateModel.instance.get('lastVersion'); if (!Launcher || ver === RuntimeInfo.version) { @@ -164,6 +183,14 @@ var Updater = { cb(); }); }); + }, + + checkAppCacheUpdateReady: function() { + if (window.applicationCache.status === window.applicationCache.UPDATEREADY) { + try { window.applicationCache.swapCache(); } catch (e) { } + UpdateModel.instance.set('updateStatus', 'ready'); + Backbone.trigger('update-app'); + } } }; diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index aa93d4a4..6faad137 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -3,9 +3,9 @@ var Links = { Repo: 'https://github.com/antelle/keeweb', Desktop: 'https://github.com/antelle/keeweb/releases/latest', - WebApp: 'http://localhost:8088/', + WebApp: 'http://antelle.github.io/keeweb/', License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt', - UpdateDesktop: 'http://localhost:8088/releases/download/{ver}/UpdateDesktop.zip' + UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip' }; module.exports = Links; diff --git a/app/scripts/models/update-model.js b/app/scripts/models/update-model.js index b0c4daf5..00567524 100644 --- a/app/scripts/models/update-model.js +++ b/app/scripts/models/update-model.js @@ -11,7 +11,8 @@ var UpdateModel = Backbone.Model.extend({ lastCheckError: null, status: null, updateStatus: null, - updateError: null + updateError: null, + updateManual: false }, initialize: function() { @@ -33,8 +34,11 @@ var UpdateModel = Backbone.Model.extend({ save: function() { var attr = _.clone(this.attributes); - delete attr.updateStatus; - delete attr.updateError; + Object.keys(attr).forEach(function(key) { + if (key.lastIndexOf('update', 0) === 0) { + delete attr[key]; + } + }); localStorage.updateInfo = JSON.stringify(attr); } }); diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index 14ec5937..d40cb2a0 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -105,9 +105,7 @@ var AppView = Backbone.View.extend({ }, updateApp: function() { - if (this.model.files.hasOpenFiles()) { - this.showUpdateBubble(); // TODO - } else { + if (!Launcher && !this.model.files.hasOpenFiles()) { window.location.reload(); } }, diff --git a/app/scripts/views/footer-view.js b/app/scripts/views/footer-view.js index 495a531b..d2a1c01e 100644 --- a/app/scripts/views/footer-view.js +++ b/app/scripts/views/footer-view.js @@ -3,7 +3,8 @@ var Backbone = require('backbone'), Keys = require('../const/keys'), KeyHandler = require('../comp/key-handler'), - GeneratorView = require('./generator-view'); + GeneratorView = require('./generator-view'), + UpdateModel = require('../models/update-model'); var FooterView = Backbone.View.extend({ template: require('templates/footer.html'), @@ -28,10 +29,15 @@ var FooterView = Backbone.View.extend({ KeyHandler.onKey(Keys.DOM_VK_COMMA, this.toggleSettings, this, KeyHandler.SHORTCUT_ACTION); this.listenTo(this.model.files, 'update reset change', this.render); + this.listenTo(Backbone, 'update-app', this.render); }, render: function () { - this.$el.html(this.template(this.model)); + this.listenTo(Backbone, 'update-app', this.updateApp); + this.$el.html(this.template({ + files: this.model.files, + updateAvailable: UpdateModel.instance.get('updateStatus') === 'ready' + })); return this; }, diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 2282084b..f8003fe9 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -6,7 +6,8 @@ var Backbone = require('backbone'), Format = require('../../util/format'), AppSettingsModel = require('../../models/app-settings-model'), UpdateModel = require('../../models/update-model'), - RuntimeInfo = require('../../comp/runtime-info'); + RuntimeInfo = require('../../comp/runtime-info'), + Links = require('../../const/links'); var SettingsGeneralView = Backbone.View.extend({ template: require('templates/settings/settings-general.html'), @@ -17,6 +18,7 @@ var SettingsGeneralView = Backbone.View.extend({ 'change .settings__general-auto-update': 'changeAutoUpdate', 'click .settings__general-update-btn': 'checkUpdate', 'click .settings__general-restart-btn': 'restartApp', + 'click .settings__general-download-update-btn': 'downloadUpdate', 'click .settings__general-dev-tools-link': 'openDevTools' }, @@ -41,7 +43,8 @@ var SettingsGeneralView = Backbone.View.extend({ autoUpdate: Updater.enabledAutoUpdate(), updateInProgress: Updater.updateInProgress(), updateInfo: this.getUpdateInfo(), - updateReady: UpdateModel.instance.get('updateStatus') === 'ready' + updateReady: UpdateModel.instance.get('updateStatus') === 'ready', + updateManual: UpdateModel.instance.get('updateManual') }); }, @@ -99,7 +102,15 @@ var SettingsGeneralView = Backbone.View.extend({ }, restartApp: function() { - Launcher.requestRestart(); + if (Launcher) { + Launcher.requestRestart(); + } else { + window.location.reload(); + } + }, + + downloadUpdate: function() { + Launcher.openLink(Links.Desktop); }, changeExpandGroups: function(e) { diff --git a/app/styles/areas/_footer.scss b/app/styles/areas/_footer.scss index 9725a84e..3521075d 100644 --- a/app/styles/areas/_footer.scss +++ b/app/styles/areas/_footer.scss @@ -41,4 +41,9 @@ padding: $base-padding; font-size: 1.4em; } + + &__update-icon { + @include th { color: action-color(); } + @include animation(shake 50s cubic-bezier(.36,.07,.19,.97) 0s infinite); + } } diff --git a/app/styles/areas/_settings.scss b/app/styles/areas/_settings.scss index 2f6c0789..9413f6d5 100644 --- a/app/styles/areas/_settings.scss +++ b/app/styles/areas/_settings.scss @@ -77,8 +77,6 @@ } &__general-update-btn { width: 15em; - } - &__general-restart-btn { - margin-left: $small-spacing; + margin-right: $small-spacing; } } diff --git a/app/styles/base/_colors.scss b/app/styles/base/_colors.scss index b0bb17b4..6f31bd77 100644 --- a/app/styles/base/_colors.scss +++ b/app/styles/base/_colors.scss @@ -57,6 +57,5 @@ $all-colors: ( @each $col, $val in $all-colors { .#{$col}-color { color: #{$val}; } } -.muted-color { - @include th { color: muted-color(); }; -} +.muted-color { @include th { color: muted-color(); }; } +.action-color { @include th { color: action-color(); }; } diff --git a/app/styles/common/_fx.scss b/app/styles/common/_fx.scss index 5c994893..6c9ab9ad 100644 --- a/app/styles/common/_fx.scss +++ b/app/styles/common/_fx.scss @@ -25,3 +25,11 @@ @include transform(rotateY(360deg)); } } + +@include keyframes(shake) { + 0%, 1%, 100% { @include transform(translate3d(0, 0, 0)); } + .1%, .9% { @include transform(translate3d(-1px, 0, 0)); } + .2%, .8% { @include transform(translate3d(2px, 0, 0)); } + .3%, .5%, .7% { @include transform(translate3d(-3px, 0, 0)); } + .4%, .6% { @include transform(translate3d(3px, 0, 0)); } +} diff --git a/app/templates/footer.html b/app/templates/footer.html index c548e899..62617653 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -9,7 +9,13 @@ - + diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index af1b1bf1..739d1ced 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -1,5 +1,19 @@

General Settings

+ <% if (updateReady && !canAutoUpdate) { %> +

Update

+
New app version was released and downloaded.
+
+ +
+ <% } else if (updateManual) { %> +

Update

+
New version has been released. It will check for updates and install them automatically + but auto-upgrading from your version is impossible.
+
+ +
+ <% } %>

Appearance

@@ -13,7 +27,7 @@ />
- <% if (canAutoUpdate) { %> + <% if (canAutoUpdate && !updateManual) { %>

Function

/> From cc7b4e38be02f595aad40cbafd66a78fe5fbaa0e Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 18:31:39 +0300 Subject: [PATCH 09/14] readme --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc69e7e0..be218a57 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,13 @@ You can download the latest distribution files from [gh-pages](https://github.co # Building -The app can be built with grunt: `grunt` (html file will be in `dist/`) or `grunt watch` (result will be in `tmp/`). -Electron app is built with `grunt electron` (works only under mac osx as it builds dmg; requires wine). -To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start with `electron ./electron/`. +The app can be built with grunt: `grunt` (html file will be in `dist/`) or `grunt watch` (debug version; the result will be in `tmp/`). +Desktop apps are built with `grunt desktop`. This works only in mac osx as it builds dmg; requires wine. +To run Electron app without building, install electron package (`npm install electron-prebuilt -g`) and start in this way: +```bash +$ cd electron +$ electron . --htmlpath=../tmp +``` # Contributing From a8b929345337e77a1e07e929c9fe28e047f4142b Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 18:32:13 +0300 Subject: [PATCH 10/14] readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index be218a57..3483ce20 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Reading and display is mostly complete; modification and sync is under construct These major issues are in progress, or will be fixed in next releases, before v1.0: -- auto-update is not implemented - dropbox sync is one-way: changes are not loaded from dropbox, only saved # Self-hosting From 72bf952717f4faa9a43dc6c64f52a4a66d63b482 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 18:47:51 +0300 Subject: [PATCH 11/14] desktop archive validation on build --- Gruntfile.js | 9 ++++++ grunt/tasks/grunt-validate-desktop-update.js | 31 ++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 grunt/tasks/grunt-validate-desktop-update.js diff --git a/Gruntfile.js b/Gruntfile.js index 1603816e..c156a67a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -348,6 +348,14 @@ module.exports = function(grunt) { options: { archive: 'dist/desktop/UpdateDesktop.zip' }, files: [{ cwd: 'tmp/desktop/app', src: '**', expand: true }] } + }, + 'validate-desktop-update': { + desktop: { + options: { + file: 'dist/desktop/UpdateDesktop.zip', + expected: ['main.js', 'app.js', 'index.html', 'package.json', 'node_modules/node-stream-zip/node_stream_zip.js'] + } + } } }); @@ -376,6 +384,7 @@ module.exports = function(grunt) { 'copy:desktop_app_content', 'string-replace:desktop_html', 'compress:desktop_update', + 'validate-desktop-update', 'electron', 'electron_builder', 'compress:linux', diff --git a/grunt/tasks/grunt-validate-desktop-update.js b/grunt/tasks/grunt-validate-desktop-update.js new file mode 100644 index 00000000..17246313 --- /dev/null +++ b/grunt/tasks/grunt-validate-desktop-update.js @@ -0,0 +1,31 @@ +'use strict'; + +module.exports = function (grunt) { + grunt.registerMultiTask('validate-desktop-update', 'Validates desktop update package', function () { + var path = require('path'); + var done = this.async(); + var StreamZip = require(path.resolve(__dirname, '../../electron/node_modules/node-stream-zip')); + var zip = new StreamZip({ file: this.options().file, storeEntries: true }); + var expFiles = this.options().expected; + zip.on('error', function(err) { + grunt.warn(err); + }); + zip.on('ready', function() { + var valid = true; + expFiles.forEach(function(entry) { + try { + if (!zip.entryDataSync(entry)) { + grunt.warn('Corrupted entry in desktop update archive: ' + entry); + valid = false; + } + } catch (e) { + grunt.warn('Entry not found in desktop update archive: ' + entry); + valid = false; + } + }); + if (valid) { + done(); + } + }); + }); +}; From d0f5821fd4f235c9d67194d6c6142028b3a7105f Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 19:07:14 +0300 Subject: [PATCH 12/14] release notes link --- app/scripts/comp/updater.js | 1 + app/scripts/const/links.js | 3 ++- app/scripts/views/settings/settings-general-view.js | 3 ++- app/templates/settings/settings-general.html | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/comp/updater.js b/app/scripts/comp/updater.js index 45f45030..8ed88b17 100644 --- a/app/scripts/comp/updater.js +++ b/app/scripts/comp/updater.js @@ -122,6 +122,7 @@ var Updater = { checkManualDownload: function() { if (+Launcher.getAppVersion().split('.')[1] <= 2) { UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true }); + return true; } }, diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index 6faad137..a13c89e6 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -5,7 +5,8 @@ var Links = { Desktop: 'https://github.com/antelle/keeweb/releases/latest', WebApp: 'http://antelle.github.io/keeweb/', License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt', - UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip' + UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip', + ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes' }; module.exports = Links; diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index f8003fe9..fa0361b7 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -44,7 +44,8 @@ var SettingsGeneralView = Backbone.View.extend({ updateInProgress: Updater.updateInProgress(), updateInfo: this.getUpdateInfo(), updateReady: UpdateModel.instance.get('updateStatus') === 'ready', - updateManual: UpdateModel.instance.get('updateManual') + updateManual: UpdateModel.instance.get('updateManual'), + releaseNotesLink: Links.ReleaseNotes }); }, diff --git a/app/templates/settings/settings-general.html b/app/templates/settings/settings-general.html index 739d1ced..d694394d 100644 --- a/app/templates/settings/settings-general.html +++ b/app/templates/settings/settings-general.html @@ -2,7 +2,7 @@

General Settings

<% if (updateReady && !canAutoUpdate) { %>

Update

-
New app version was released and downloaded.
+
New app version was released and downloaded. View release notes
@@ -33,6 +33,7 @@ />
<%- updateInfo %>
+ View release notes
<% if (updateInProgress) { %> From 06ba534cba79b8e7c6345111bdd4e83b3fc34ed1 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 19:08:31 +0300 Subject: [PATCH 13/14] https link --- app/scripts/const/links.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index a13c89e6..5d329ed6 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -3,7 +3,7 @@ var Links = { Repo: 'https://github.com/antelle/keeweb', Desktop: 'https://github.com/antelle/keeweb/releases/latest', - WebApp: 'http://antelle.github.io/keeweb/', + WebApp: 'https://antelle.github.io/keeweb/', License: 'https://github.com/antelle/keeweb/blob/master/MIT-LICENSE.txt', UpdateDesktop: 'https://github.com/antelle/keeweb/releases/download/{ver}/UpdateDesktop.zip', ReleaseNotes: 'https://github.com/antelle/keeweb/blob/master/release-notes.md#release-notes' From 5ad5cbd6e459b52a923e8f066e105b2fc023c040 Mon Sep 17 00:00:00 2001 From: Antelle Date: Sat, 14 Nov 2015 19:58:34 +0300 Subject: [PATCH 14/14] user-agent in transport --- app/scripts/comp/transport.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scripts/comp/transport.js b/app/scripts/comp/transport.js index ef70d473..21e6928b 100644 --- a/app/scripts/comp/transport.js +++ b/app/scripts/comp/transport.js @@ -23,7 +23,9 @@ var Transport = { } var proto = config.url.split(':')[0]; console.log('GET ' + config.url); - Launcher.req(proto).get(config.url, function(res) { + var opts = Launcher.req('url').parse(config.url); + opts.headers = { 'User-Agent': navigator.userAgent }; + Launcher.req(proto).get(opts, function(res) { console.log('Response from ' + config.url + ': ' + res.statusCode); if (res.statusCode === 200) { if (config.file) {