loading saved plugins

This commit is contained in:
antelle 2017-02-19 18:51:52 +01:00
parent 664e7c50ae
commit 35d74033fd
6 changed files with 307 additions and 141 deletions

View File

@ -11,6 +11,7 @@ const Updater = require('./comp/updater');
const AuthReceiver = require('./comp/auth-receiver');
const ExportApi = require('./comp/export-api');
const SettingsManager = require('./comp/settings-manager');
const PluginManager = require('./plugins/plugin-manager');
const KdbxwebInit = require('./util/kdbxweb-init');
const Locale = require('./util/locale');
@ -19,23 +20,24 @@ $(() => {
return AuthReceiver.receive();
}
loadMixins();
initModules();
const appModel = new AppModel();
SettingsManager.setBySettings(appModel.settings);
const configParam = getConfigParam();
if (configParam) {
appModel.loadConfig(configParam, err => {
SettingsManager.setBySettings(appModel.settings);
if (err && !appModel.settings.get('cacheConfigSettings')) {
showSettingsLoadError();
} else {
showApp();
}
});
} else {
showApp();
}
let appModel;
initModules().then(() => {
appModel = new AppModel();
SettingsManager.setBySettings(appModel.settings);
const configParam = getConfigParam();
if (configParam) {
appModel.loadConfig(configParam, err => {
SettingsManager.setBySettings(appModel.settings);
if (err && !appModel.settings.get('cacheConfigSettings')) {
showSettingsLoadError();
} else {
showApp();
}
});
} else {
showApp();
}
});
function isPopup() {
return (window.parent !== window.top) || window.opener;
@ -47,11 +49,14 @@ $(() => {
}
function initModules() {
const promises = [];
KeyHandler.init();
IdleTracker.init();
PopupNotifier.init();
KdbxwebInit.init();
promises.push(PluginManager.init());
window.kw = ExportApi;
return Promise.all(promises);
}
function showSettingsLoadError() {

View File

@ -3,6 +3,8 @@
const Backbone = require('backbone');
const Plugin = require('./plugin');
const PluginCollection = require('./plugin-collection');
const SettingsStore = require('../comp/settings-store');
const Logger = require('../util/logger');
const PluginManager = Backbone.Model.extend({
defaults: {
@ -13,14 +15,33 @@ const PluginManager = Backbone.Model.extend({
plugins: new PluginCollection()
},
logger: new Logger('plugin-mgr'),
init() {
const ts = this.logger.ts();
return Promise.resolve().then(() => {
const state = this.loadState();
if (!state || !state.plugins) {
return;
}
const promises = state.plugins.map(plugin => this.loadPlugin(plugin));
return Promise.all(promises).then(loadedPlugins => {
const plugins = this.get('plugins');
plugins.add(loadedPlugins.filter(plugin => plugin));
this.logger.info(`Loaded ${plugins.length} plugins`, this.logger.ts(ts));
});
});
},
install(url) {
const lastInstall = { url, dt: new Date() };
this.set({ installing: url, lastInstall: lastInstall });
return Plugin.load(url).then(plugin =>
return Plugin.loadFromUrl(url).then(plugin =>
this.uninstall(plugin.id).then(() => {
return plugin.install().then(() => {
this.get('plugins').push(plugin);
this.set({ installing: null });
this.saveState();
});
})
).catch(e => {
@ -40,6 +61,26 @@ const PluginManager = Backbone.Model.extend({
plugins.remove(id);
this.set('uninstalling', null);
});
},
loadPlugin(desc) {
const plugin = new Plugin(desc.manifest, desc.url, true);
return plugin.install()
.then(() => plugin)
.catch(() => undefined);
},
saveState() {
SettingsStore.save('plugins', {
plugins: this.get('plugins').map(plugin => ({
manifest: plugin.get('manifest'),
url: plugin.get('url')
}))
});
},
loadState() {
return SettingsStore.load('plugins');
}
});

View File

@ -5,8 +5,13 @@ const Backbone = require('backbone');
const PluginApi = require('./plugin-api');
const Logger = require('../util/logger');
const SettingsManager = require('../comp/settings-manager');
const IoCache = require('../storage/io-cache');
const commonLogger = new Logger('plugin');
const io = new IoCache({
cacheName: 'PluginFiles',
logger: new Logger('storage-plugin-files')
});
const Plugin = Backbone.Model.extend({
idAttribute: 'name',
@ -15,21 +20,25 @@ const Plugin = Backbone.Model.extend({
name: '',
manifest: '',
url: '',
status: 'inactive'
status: 'inactive',
installTime: null
},
resources: null,
initialize(manifest, url) {
initialize(manifest, url, local) {
const name = manifest.name;
this.set({
name: manifest.name,
name,
manifest,
url
url,
local
});
this.logger = new Logger(`plugin:${manifest.name}`);
this.logger = new Logger(`plugin:${name}`);
},
install() {
const ts = this.logger.ts();
this.set('status', 'installing');
return Promise.resolve().then(() => {
const error = this.validateManifest();
@ -38,7 +47,8 @@ const Plugin = Backbone.Model.extend({
this.set('status', 'invalid');
throw 'Plugin validation error: ' + error;
}
return this.installWithManifest();
return this.installWithManifest()
.then(this.set('installTime', this.logger.ts() - ts));
});
},
@ -76,29 +86,46 @@ const Plugin = Backbone.Model.extend({
installWithManifest() {
const manifest = this.get('manifest');
const url = this.get('url');
this.logger.info('Loading plugin with resources', Object.keys(manifest.resources).join(', '));
const local = this.get('local');
this.logger.info('Loading plugin with resources', Object.keys(manifest.resources).join(', '), local ? '(local)' : '(url)');
this.resources = {};
const ts = this.logger.ts();
const results = [];
if (manifest.resources.css) {
results.push(this.loadResource('css', url + 'plugin.css'));
}
if (manifest.resources.js) {
results.push(this.loadResource('js', url + 'plugin.js'));
}
if (manifest.resources.loc) {
results.push(this.loadResource('loc', url + manifest.locale.name + '.json'));
}
const results = Object.keys(manifest.resources)
.map(res => this.loadResource(res));
return Promise.all(results)
.catch(() => { throw 'Error loading plugin resources'; })
.then(() => this.installWithResources())
.then(() => {
this.logger.info('Install complete', this.logger.ts(ts));
});
.then(() => local ? undefined : this.saveResources())
.then(() => { this.logger.info('Install complete', this.logger.ts(ts)); });
},
loadResource(type, url) {
getResourcePath(res) {
switch (res) {
case 'css':
return 'plugin.css';
case 'js':
return 'plugin.js';
case 'loc':
return this.get('manifest').locale.name + '.json';
default:
throw `Unknown resource ${res}`;
}
},
loadResource(type) {
let res;
if (this.get('local')) {
res = this.loadLocalResource(type);
} else {
const url = this.get('url');
res = this.loadResourceFromUrl(type, url + this.getResourcePath(type));
}
return res.then(data => {
this.resources[type] = data;
});
},
loadResourceFromUrl(type, url) {
let ts = this.logger.ts();
const manifest = this.get('manifest');
return httpGet(url, true).then(data => {
@ -116,7 +143,7 @@ const Plugin = Backbone.Model.extend({
.then(valid => {
if (valid) {
this.logger.debug('Resource signature valid', type, this.logger.ts(ts));
this.resources[type] = data;
return data;
} else {
this.logger.error('Resource signature invalid', type);
throw `Signature invalid: ${type}`;
@ -125,6 +152,19 @@ const Plugin = Backbone.Model.extend({
});
},
loadLocalResource(type) {
return new Promise((resolve, reject) => {
const storageKey = this.id + '/' + this.getResourcePath(type);
io.load(storageKey, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
},
installWithResources() {
this.logger.info('Installing loaded plugin');
const manifest = this.get('manifest');
@ -144,11 +184,50 @@ const Plugin = Backbone.Model.extend({
})
.catch(e => {
this.logger.info('Install error', e);
this.uninstall();
throw e;
return this.uninstall().then(() => { throw e; });
});
},
saveResources() {
const resourceSavePromises = [];
for (const key of Object.keys(this.resources)) {
resourceSavePromises.push(this.saveResource(key, this.resources[key]));
}
return Promise.all(resourceSavePromises)
.catch(e => {
this.logger.debug('Error saving plugin resources', e);
return this.uninstall().then(() => { throw 'Error saving plugin resources'; });
});
},
saveResource(key, value) {
return new Promise((resolve, reject) => {
const storageKey = this.id + '/' + this.getResourcePath(key);
io.save(storageKey, value, e => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
},
deleteResources() {
const resourceDeletePromises = [];
for (const key of Object.keys(this.resources)) {
resourceDeletePromises.push(this.deleteResource(key));
}
return Promise.all(resourceDeletePromises);
},
deleteResource(key) {
return new Promise(resolve => {
const storageKey = this.id + '/' + this.getResourcePath(key);
io.remove(storageKey, () => resolve());
});
},
applyCss(name, data) {
return Promise.resolve().then(() => {
const text = kdbxweb.ByteUtils.bytesToString(data);
@ -240,13 +319,15 @@ const Plugin = Backbone.Model.extend({
if (manifest.resources.loc) {
this.removeLoc(this.get('manifest').locale);
}
this.set('status', 'inactive');
this.logger.info('Uninstall complete', this.logger.ts(ts));
return this.deleteResources().then(() => {
this.set('status', 'inactive');
this.logger.info('Uninstall complete', this.logger.ts(ts));
});
});
}
});
Plugin.load = function(url) {
Plugin.loadFromUrl = function(url) {
if (url[url.length - 1] !== '/') {
url += '/';
}

View File

@ -0,0 +1,111 @@
'use strict';
const idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
const IoBrowserCache = function(config) {
this.db = null;
this.cacheName = config.cacheName;
this.logger = config.logger;
};
IoBrowserCache.enabled = !!idb;
_.extend(IoBrowserCache.prototype, {
initDb(callback) {
if (this.db) {
return callback && callback();
}
try {
const req = idb.open(this.cacheName);
req.onerror = e => {
this.logger.error('Error opening indexed db', e);
if (callback) { callback(e); }
};
req.onsuccess = e => {
this.db = e.target.result;
if (callback) { callback(); }
};
req.onupgradeneeded = e => {
const db = e.target.result;
db.createObjectStore('files');
};
} catch (e) {
this.logger.error('Error opening indexed db', e);
if (callback) { callback(e); }
}
},
save(id, data, callback) {
this.logger.debug('Save', id);
this.initDb(err => {
if (err) {
return callback && callback(err);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
req.onsuccess = () => {
this.logger.debug('Saved', id, this.logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = () => {
this.logger.error('Error saving to cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error saving to cache', id, e);
if (callback) { callback(e); }
}
});
},
load(id, callback) {
this.logger.debug('Load', id);
this.initDb(err => {
if (err) {
return callback && callback(err, null);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
req.onsuccess = () => {
this.logger.debug('Loaded', id, this.logger.ts(ts));
if (callback) { callback(null, req.result); }
};
req.onerror = () => {
this.logger.error('Error loading from cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error loading from cache', id, e);
if (callback) { callback(e, null); }
}
});
},
remove(id, callback) {
this.logger.debug('Remove', id);
this.initDb(err => {
if (err) {
return callback && callback(err);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
req.onsuccess = () => {
this.logger.debug('Removed', id, this.logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = () => {
this.logger.error('Error removing from cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error removing from cache', id, e);
if (callback) { callback(e); }
}
});
}
});
module.exports = IoBrowserCache;

View File

@ -0,0 +1,7 @@
'use strict';
const Launcher = require('../comp/launcher');
const IoCache = Launcher ? require('./io-browser-cache') : require('./io-browser-cache'); // TODO: use file cache
module.exports = IoCache;

View File

@ -1,112 +1,33 @@
'use strict';
const StorageBase = require('./storage-base');
const idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
const IoBrowserCache = require('./io-browser-cache');
const StorageCache = StorageBase.extend({
name: 'cache',
enabled: !!idb,
enabled: IoBrowserCache.enabled,
system: true,
db: null,
errorOpening: null,
io: null,
initDb: function(callback) {
if (this.db) {
return callback && callback();
}
try {
const req = idb.open('FilesCache');
req.onerror = e => {
this.logger.error('Error opening indexed db', e);
this.errorOpening = e;
if (callback) { callback(e); }
};
req.onsuccess = e => {
this.db = e.target.result;
if (callback) { callback(); }
};
req.onupgradeneeded = e => {
const db = e.target.result;
db.createObjectStore('files');
};
} catch (e) {
this.logger.error('Error opening indexed db', e);
if (callback) { callback(e); }
}
},
save: function(id, opts, data, callback) {
this.logger.debug('Save', id);
this.initDb(err => {
if (err) {
return callback && callback(err);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
req.onsuccess = () => {
this.logger.debug('Saved', id, this.logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = () => {
this.logger.error('Error saving to cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error saving to cache', id, e);
if (callback) { callback(e); }
}
init() {
StorageBase.prototype.init.call(this);
this.io = new IoBrowserCache({
cacheName: 'FilesCache',
logger: this.logger
});
},
load: function(id, opts, callback) {
this.logger.debug('Load', id);
this.initDb(err => {
if (err) {
return callback && callback(err, null);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
req.onsuccess = () => {
this.logger.debug('Loaded', id, this.logger.ts(ts));
if (callback) { callback(null, req.result); }
};
req.onerror = () => {
this.logger.error('Error loading from cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error loading from cache', id, e);
if (callback) { callback(e, null); }
}
});
save(id, opts, data, callback) {
this.io.save(id, data, callback);
},
remove: function(id, opts, callback) {
this.logger.debug('Remove', id);
this.initDb(err => {
if (err) {
return callback && callback(err);
}
try {
const ts = this.logger.ts();
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
req.onsuccess = () => {
this.logger.debug('Removed', id, this.logger.ts(ts));
if (callback) { callback(); }
};
req.onerror = () => {
this.logger.error('Error removing from cache', id, req.error);
if (callback) { callback(req.error); }
};
} catch (e) {
this.logger.error('Error removing from cache', id, e);
if (callback) { callback(e); }
}
});
load(id, opts, callback) {
this.io.load(id, callback);
},
remove(id, opts, callback) {
this.io.remove(id, callback);
}
});