sync providers wip

This commit is contained in:
Mo Bitar
2017-01-24 20:55:55 -06:00
parent c656024959
commit 13e6ac59a9
6 changed files with 258 additions and 78 deletions

View File

@@ -71,15 +71,47 @@ angular.module('app.frontend')
})
}
this.syncProviderActionIsEnabled = function(action) {
var provider = apiController.syncProviderForURL(action.url);
if(!provider) {
return null;
}
return provider.status;
}
this.enableSyncProvider = function(action, extension, primary) {
if(extension.encrypted && !extension.ek) {
alert("You must set an encryption key for this extension before enabling this action.");
return;
}
var provider = apiController.findOrCreateSyncProviderForUrl(action.url);
provider.primary = primary;
provider.enabled = true;
provider.ek = extension.ek;
apiController.addSyncProvider(provider);
}
this.disableSyncProvider = function(action, extension) {
apiController.removeSyncProvider(apiController.syncProviderForURL(action.url));
}
this.deleteExtension = function(extension) {
if(confirm("Are you sure you want to delete this extension?")) {
extensionManager.deleteExtension(extension);
var syncProviderAction = extension.syncProviderAction;
if(syncProviderAction) {
apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
}
}
}
this.reloadExtensionsPressed = function() {
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) {
extensionManager.refreshExtensionsFromServer();
var syncProviderAction = extension.syncProviderAction;
if(syncProviderAction) {
apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url));
}
}
}

View File

@@ -56,9 +56,13 @@ class Extension extends Item {
this.content_type = "Extension";
}
syncProviderAction() {
return _.find(this.actions, {sync_provider: true})
}
actionsInGlobalContext() {
return this.actions.filter(function(action){
return action.context == "global";
return action.context == "global" || action.sync_provider == true;
})
}

View File

@@ -0,0 +1,36 @@
class SyncProvider {
constructor(obj) {
_.merge(this, obj);
}
addPendingItems(items) {
if(!this.pendingItems) {
this.pendingItems = [];
}
this.pendingItems = this.pendingItems.concat(items);
}
removePendingItems(items) {
this.pendingItems = _.difference(this.pendingItems, items);
}
get status() {
if(!this.enabled) {
return null;
}
if(this.primary) return "primary";
else return "secondary";
}
asJSON() {
return {
enabled: this.enabled,
url: this.url,
encrypted: this.encrypted,
ek: this.ek
}
}
}

View File

@@ -201,9 +201,71 @@ angular.module('app.frontend')
/*
Items
Sync
*/
this.syncProviderForURL = function(url) {
return _.find(this.syncProviders, {url: url});
}
this.findOrCreateSyncProviderForUrl = function(url) {
var provider = _.find(this.syncProviders, {url: url});
if(!provider) {
provider = new SyncProvider({url: url})
}
return provider;
}
this.setEncryptionStatusForProviderURL = function(providerURL, encrypted) {
this.providerForURL(providerURL).encrypted = encrypted;
this.persistSyncProviders();
}
this.loadSyncProviders = function() {
var providers = [];
var saved = localStorage.getItem("syncProviders");
if(saved) {
var parsed = JSON.parse(saved);
for(var p of parsed) {
providers.push(new SyncProvider(p));
}
} else {
// no providers saved, use default
if(this.isUserSignedIn()) {
var defaultProvider = new SyncProvider(this.getServer() + "/items/sync", true);
providers.push(defaultProvider);
}
}
this.syncProviders = providers;
}
this.loadSyncProviders();
this.addSyncProvider = function(syncProvider) {
if(syncProvider.primary) {
for(var provider of this.syncProviders) {
provider.primary = false;
}
}
// since we're adding a new provider, we need to send it EVERYTHING we have now.
syncProvider.addPendingItems(modelManager.allItems);
this.syncProviders.push(syncProvider);
this.persistSyncProviders();
}
this.removeSyncProvider = function(provider) {
_.pull(this.syncProviders, provider);
this.persistSyncProviders();
}
this.persistSyncProviders = function() {
localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
return provider.asJSON()
})));
}
this.setSyncToken = function(syncToken) {
this.syncToken = syncToken;
localStorage.setItem("syncToken", this.syncToken);
@@ -211,14 +273,6 @@ angular.module('app.frontend')
this.syncWithOptions = function(callback, options = {}) {
if(this.syncOpInProgress) {
// will perform anoter sync after current completes
this.repeatSync = true;
return;
}
this.syncOpInProgress = true;
var allDirtyItems = modelManager.getDirtyItems();
// we want to write all dirty items to disk only if the user is not signed in, or if the sync op fails
@@ -243,55 +297,94 @@ angular.module('app.frontend')
modelManager.clearDirtyItems(allDirtyItems);
}.bind(this))
this.syncOpInProgress = false;
if(callback) {
if(this.syncProviders.length == 0 && callback) {
callback();
}
}
for(let provider of this.syncProviders) {
if(provider.enabled == false) {
continue;
}
provider.addPendingItems(allDirtyItems);
this.__performSyncWithProvider(provider, options, function(response){
if(provider.primary) {
if(callback) {
callback(response)
}
}
})
}
modelManager.clearDirtyItems(allDirtyItems);
}
this.__performSyncWithProvider = function(provider, options, callback) {
if(provider.syncOpInProgress) {
provider.repeatOnCompletion = true;
console.log("Sync op in progress for provider; returning.", provider);
return;
}
provider.syncOpInProgress = true;
let submitLimit = 100;
var dirtyItems = allDirtyItems.slice(0, submitLimit);
if(dirtyItems.length < allDirtyItems.length) {
var allItems = provider.pendingItems;
var subItems = allItems.slice(0, submitLimit);
if(subItems.length < allItems.length) {
// more items left to be synced, repeat
this.repeatSync = true;
provider.repeatOnCompletion = true;
} else {
this.repeatSync = false;
provider.repeatOnCompletion = false;
}
var request = Restangular.one("items/sync");
console.log("Syncing with provider", provider, subItems);
// Remove dirty items now. If this operation fails, we'll re-add them.
// This allows us to queue changes on the same item
provider.removePendingItems(subItems);
var request = Restangular.oneUrl(provider.url, provider.url);
request.limit = 150;
request.sync_token = this.syncToken;
request.cursor_token = this.cursorToken;
request.items = _.map(dirtyItems, function(item){
return this.createRequestParamsForItem(item, options.additionalFields);
request.items = _.map(subItems, function(item){
return this.paramsForItem(item, provider.encrypted, provider.ek, options.additionalFields, false);
}.bind(this));
if(provider.primary) {
// only primary providers receive items (or care about received items)
request.sync_token = this.syncToken;
request.cursor_token = provider.cursorToken;
}
request.post().then(function(response) {
modelManager.clearDirtyItems(dirtyItems);
if(provider.primary) {
// handle sync token
this.setSyncToken(response.sync_token);
$rootScope.$broadcast("sync:updated_token", this.syncToken);
// handle sync token
this.setSyncToken(response.sync_token);
$rootScope.$broadcast("sync:updated_token", this.syncToken);
// handle cursor token (more results waiting, perform another sync)
provider.cursorToken = response.cursor_token;
// handle cursor token (more results waiting, perform another sync)
this.cursorToken = response.cursor_token;
var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider);
// merge only metadata for saved items
var omitFields = ["content", "auth_hash"];
var saved = this.handleItemsResponse(response.saved_items, omitFields, provider);
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
// merge only metadata for saved items
var omitFields = ["content", "auth_hash"];
var saved = this.handleItemsResponse(response.saved_items, omitFields);
this.handleUnsavedItemsResponse(response.unsaved, provider)
this.handleUnsavedItemsResponse(response.unsaved)
this.writeItemsToLocalStorage(saved, null);
this.writeItemsToLocalStorage(retrieved, null);
}
this.writeItemsToLocalStorage(saved, null);
this.writeItemsToLocalStorage(retrieved, null);
provider.syncOpInProgress = false;
this.syncOpInProgress = false;
if(this.cursorToken || this.repeatSync == true) {
this.syncWithOptions(callback, options);
if(provider.cursorToken || provider.repeatOnCompletion == true) {
this.__performSyncWithProvider(provider, options, callback);
} else {
if(callback) {
callback(response);
@@ -302,8 +395,13 @@ angular.module('app.frontend')
.catch(function(response){
console.log("Sync error: ", response);
writeAllDirtyItemsToDisk();
this.syncOpInProgress = false;
// Re-add subItems since this operation failed. We'll have to try again.
provider.addPendingItems(subItems);
provider.syncOpInProgress = false;
if(provider.primary) {
this.writeItemsToLocalStorage(allItems, null);
}
if(callback) {
callback({error: "Sync error"});
@@ -315,7 +413,7 @@ angular.module('app.frontend')
this.syncWithOptions(callback, undefined);
}
this.handleUnsavedItemsResponse = function(unsaved) {
this.handleUnsavedItemsResponse = function(unsaved, provider) {
if(unsaved.length == 0) {
return;
}
@@ -332,19 +430,15 @@ angular.module('app.frontend')
}
}
this.syncWithOptions(null, {additionalFields: ["created_at", "updated_at"]});
this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null);
}
this.handleItemsResponse = function(responseItems, omitFields) {
this.decryptItems(responseItems);
this.handleItemsResponse = function(responseItems, omitFields, syncProvider) {
this.decryptItemsWithKey(responseItems, syncProvider ? syncProvider.ek : null);
return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
}
this.createRequestParamsForItem = function(item, additionalFields) {
return this.paramsForItem(item, true, additionalFields, false);
}
this.paramsForExportFile = function(item, encrypted) {
this.paramsForExportFile = function(item, ek, encrypted) {
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
}
@@ -352,15 +446,18 @@ angular.module('app.frontend')
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
}
this.paramsForItem = function(item, encrypted, additionalFields, forExportFile) {
this.paramsForItem = function(item, encrypted, ek, additionalFields, forExportFile) {
var itemCopy = _.cloneDeep(item);
if(encrypted) {
console.assert(ek.length, "Attempting to encrypt without encryption key.");
}
console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy)
var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted};
if(encrypted) {
this.encryptSingleItem(itemCopy, this.retrieveMk());
this.encryptSingleItem(itemCopy, ek);
params.content = itemCopy.content;
params.enc_item_key = itemCopy.enc_item_key;
params.auth_hash = itemCopy.auth_hash;
@@ -475,7 +572,7 @@ angular.module('app.frontend')
this.loadLocalItems = function(callback) {
var params = dbManager.getAllItems(function(items){
var items = this.handleItemsResponse(items, null);
var items = this.handleItemsResponse(items, null, null);
Item.sortItemsByDate(items);
callback(items);
}.bind(this))
@@ -560,11 +657,6 @@ angular.module('app.frontend')
item.content = content;
}
this.decryptItems = function(items) {
var masterKey = this.retrieveMk();
this.decryptItemsWithKey(items, masterKey);
}
this.decryptItemsWithKey = function(items, key) {
for (var item of items) {
if(item.deleted == true) {