Updater implementation (WIP)

This commit is contained in:
Antelle 2015-10-30 00:20:01 +03:00
parent 308ff8505c
commit b0a9cc90df
7 changed files with 206 additions and 78 deletions

View File

@ -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();
}
});

View File

@ -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) {

View File

@ -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');
}
}
};

View File

@ -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;

View File

@ -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();

View File

@ -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();
}
},

View File

@ -14,7 +14,7 @@
<div>
<input type="checkbox" class="settings__input" id="settings__general-auto-update" <%- autoUpdate ? 'checked' : '' %> />
<label for="settings__general-auto-update">Automatic updates</label>
<div>Last update check: <%- lastUpdateCheck %></div>
<div><%- updateInfo %></div>
</div>
<% } %>
<% if (devTools) { %>