mirror of https://github.com/keeweb/keeweb
Updater implementation (WIP)
parent
308ff8505c
commit
b0a9cc90df
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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) { %>
|
||||
|
|
Loading…
Reference in New Issue