sync provider wip

This commit is contained in:
Mo Bitar
2017-01-25 19:44:48 -06:00
parent 51012d7d54
commit b2200c5707
17 changed files with 173 additions and 145 deletions

View File

@@ -20,7 +20,7 @@ angular.module('app.frontend')
* Insert 4 spaces when a tab key is pressed,
* only used when inside of the text editor.
* If the shift key is pressed first, this event is
* not fired.
* not fired.
*/
var handleTab = function (event) {
if (!event.shiftKey && event.which == 9) {
@@ -146,8 +146,6 @@ angular.module('app.frontend')
note.dummy = false;
this.save()(note, function(success){
if(success) {
apiController.clearDraft();
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
this.noteStatus = "All changes saved"
@@ -171,10 +169,6 @@ angular.module('app.frontend')
this.changesMade = function() {
this.note.hasChanges = true;
this.note.dummy = false;
if(apiController.isUserSignedIn()) {
// signed out users have local autosave, dont need draft saving
apiController.saveDraftToDisk(this.note);
}
if(saveTimeout) $timeout.cancel(saveTimeout);
if(statusTimeout) $timeout.cancel(statusTimeout);
@@ -236,7 +230,6 @@ angular.module('app.frontend')
}
this.deleteNote = function() {
apiController.clearDraft();
this.remove()(this.note);
this.showMenu = false;
}

View File

@@ -21,7 +21,7 @@ angular.module('app.frontend')
this.user = apiController.user;
this.accountMenuPressed = function() {
this.serverData = {url: apiController.getServer()};
this.serverData = {url: syncManager.serverURL()};
this.showAccountMenu = !this.showAccountMenu;
this.showFaq = false;
this.showNewPasswordForm = false;

View File

@@ -7,9 +7,9 @@ angular.module('app.frontend')
syncManager.sync(null);
// refresh every 30s
setInterval(function () {
syncManager.sync(null);
}, 30000);
// setInterval(function () {
// syncManager.sync(null);
// }, 30000);
});
$scope.allTag = new Tag({all: true});

View File

@@ -48,14 +48,8 @@ angular.module('app.frontend')
if(isFirstLoad) {
$timeout(function(){
var draft = apiController.getDraft();
if(draft) {
var note = draft;
this.selectNote(note);
} else {
this.createNewNote();
isFirstLoad = false;
}
this.createNewNote();
isFirstLoad = false;
}.bind(this))
} else if(tag.notes.length == 0) {
this.createNewNote();

View File

@@ -7,7 +7,7 @@ class ItemParams {
}
paramsForExportFile() {
this.additionalFields = ["created_at", "updated_at"];
this.additionalFields = ["updated_at"];
this.forExportFile = true;
return _.omit(this.__params(), ["deleted"]);
}
@@ -16,16 +16,22 @@ class ItemParams {
return this.paramsForExportFile();
}
paramsForLocalStorage() {
this.additionalFields = ["updated_at", "dirty"];
this.forExportFile = true;
return this.__params();
}
paramsForSync() {
return __params(null, false);
return this.__params(null, false);
}
__params() {
var itemCopy = _.cloneDeep(this.item);
console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy)
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted};
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
if(this.ek) {
this.encryptionHelper.encryptItem(itemCopy, this.ek);
@@ -42,7 +48,7 @@ class ItemParams {
}
if(this.additionalFields) {
_.merge(params, _.pick(item, this.additionalFields));
_.merge(params, _.pick(this.item, this.additionalFields));
}
return params;

View File

@@ -28,18 +28,10 @@ angular.module('app.frontend')
Auth
*/
this.defaultServerURL = function() {
return localStorage.getItem("server") || "https://n3.standardnotes.org";
}
this.getAuthParams = function() {
return JSON.parse(localStorage.getItem("auth_params"));
}
this.isUserSignedIn = function() {
return localStorage.getItem("jwt");
}
this.getAuthParamsForEmail = function(url, email, callback) {
var requestUrl = url + "/auth/params";
var request = Restangular.oneUrl(requestUrl, requestUrl);
@@ -103,7 +95,7 @@ angular.module('app.frontend')
localStorage.setItem("jwt", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
syncManager.addKey(syncManager.SNKeyName, mk);
keyManager.addKey(SNKeyName, mk);
syncManager.addStandardFileSyncProvider(url);
}
@@ -119,6 +111,7 @@ angular.module('app.frontend')
callback(response);
}.bind(this))
.catch(function(response){
console.log("Registration error", response);
callback(response.data);
})
}.bind(this));
@@ -251,7 +244,7 @@ angular.module('app.frontend')
items: items
}
if(ek.name == syncManager.SNKeyName) {
if(ek.name == SNKeyName) {
// auth params are only needed when encrypted with a standard file key
data["auth_params"] = this.getAuthParams();
}
@@ -263,34 +256,27 @@ angular.module('app.frontend')
return JSON.parse(JSON.stringify(object));
}
/*
Drafts
*/
this.saveDraftToDisk = function(draft) {
localStorage.setItem("draft", JSON.stringify(draft));
}
this.clearDraft = function() {
localStorage.removeItem("draft");
}
this.getDraft = function() {
var draftString = localStorage.getItem("draft");
if(!draftString || draftString == 'undefined') {
return null;
}
var jsonObj = _.merge({content_type: "Note"}, JSON.parse(draftString));
return modelManager.createItem(jsonObj);
}
this.signoutOfStandardFile = function(callback) {
this.signoutOfStandardFile = function(destroyAll, callback) {
syncManager.removeStandardFileSyncProvider();
if(destroyAll) {
this.destroyLocalData(callback);
} else {
localStorage.removeItem("user");
localStorage.removeItem("jwt");
localStorage.removeItem("server");
localStorage.removeItem("auth_params");
callback();
}
}
this.destroyLocalData = function(callback) {
dbManager.clearAllItems(function(){
localStorage.clear();
callback();
if(callback) {
callback();
}
});
}
}
});

View File

@@ -10,6 +10,15 @@ class AccountDataMenu {
controller($scope, apiController, modelManager) {
'ngInject';
$scope.destroyLocalData = function() {
if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) {
return;
}
apiController.destroyLocalData(function(){
window.location.reload();
})
}
}
}

View File

@@ -31,10 +31,22 @@ class AccountSyncSection {
}
$scope.removeSyncProvider = function(provider) {
syncManager.removeSyncProvider(provider);
if(provider.isStandardNotesAccount) {
alert("To remove your Standard Notes sync, sign out of your Standard Notes account.")
return;
}
if(confirm("Are you sure you want to remove this sync provider?")) {
syncManager.removeSyncProvider(provider);
}
}
$scope.changeEncryptionKey = function(provider) {
if(provider.isStandardNotesAccount) {
alert("To change your encryption key for your Standard Notes account, you need to change your password. However, this functionality is not currently supported.");
return;
}
if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) {
return;
}
@@ -46,6 +58,7 @@ class AccountSyncSection {
$scope.saveKey = function(provider) {
provider.showKeyForm = false;
provider.keyName = provider.formData.keyName;
syncManager.didMakeChangesToSyncProviders();
if(provider.enabled) {
syncManager.addAllDataAsNeedingSyncForProvider(provider);

View File

@@ -7,10 +7,10 @@ class AccountVendorAccountSection {
};
}
controller($scope, apiController, modelManager, $timeout, dbManager) {
controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) {
'ngInject';
$scope.loginData = {mergeLocal: true, url: apiController.defaultServerURL()};
$scope.loginData = {mergeLocal: true, url: syncManager.serverURL()};
$scope.user = apiController.user;
$scope.changePasswordPressed = function() {
@@ -19,7 +19,7 @@ class AccountVendorAccountSection {
$scope.signOutPressed = function() {
$scope.showAccountMenu = false;
apiController.signoutOfStandardFile(function(){
apiController.signoutOfStandardFile(false, function(){
window.location.reload();
})
}

View File

@@ -14,7 +14,7 @@ class ImportExportMenu {
$scope.user = apiController.user;
$scope.downloadDataArchive = function() {
if(!apiController.isUserSignedIn() && $scope.archiveFormData.encryption_type == 'ek') {
if($scope.archiveFormData.encryption_type == 'ek') {
if(!$scope.archiveFormData.ek) {
alert("You must set an encryption key to export the data encrypted.")
return;

View File

@@ -1,26 +1,38 @@
export const SNKeyName = "Standard Notes Key";
class SyncManager {
constructor(modelManager) {
constructor(modelManager, syncRunner) {
this.modelManager = modelManager;
this.syncPerformer = new SyncPerformer()
this.SNKeyName = "Standard Notes Key";
this.syncRunner = syncRunner;
this.syncRunner.setOnChangeProviderCallback(function(){
this.didMakeChangesToSyncProviders();
}.bind(this))
this.loadSyncProviders();
}
get offline() {
return this.syncProviders.length == 0;
return this.enabledProviders.length == 0;
}
serverURL() {
return localStorage.getItem("server") || "https://n3.standardnotes.org";
}
get enabledProviders() {
return this.syncProviders.filter(function(provider){return provider.enabled == true});
}
sync(callback) {
this.syncPerformer.sync(this.syncProviders, callback);
this.syncRunner.sync(this.enabledProviders, callback);
}
syncWithProvider(provider, callback) {
this.syncPerformer.performSyncWithProvider(provider, callback);
this.syncRunner.performSyncWithProvider(provider, callback);
}
loadLocalItems(callback) {
this.syncPerformer.loadLocalItems(callback);
this.syncRunner.loadLocalItems(callback);
}
syncProviderForURL(url) {
@@ -46,16 +58,17 @@ class SyncManager {
}
removeStandardFileSyncProvider() {
var sfProvider = _.find(this.syncProviders, {url: this.defaultServerURL() + "/items/sync"})
var sfProvider = _.find(this.syncProviders, {url: this.serverURL() + "/items/sync"})
_.pull(this.syncProviders, sfProvider);
this.didMakeChangesToSyncProviders();
}
addStandardFileSyncProvider(url) {
var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0});
defaultProvider.keyName = this.SNKeyName;
var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.enabledProviders.length == 0});
defaultProvider.keyName = SNKeyName;
defaultProvider.enabled = this.syncProviders.length == 0;
this.syncProviders.push(defaultProvider);
this.didMakeChangesToSyncProviders();
return defaultProvider;
}
@@ -74,14 +87,15 @@ class SyncManager {
this.syncProviders.push(new SyncProvider(p));
}
} else {
// no providers saved, use default
if(this.isUserSignedIn()) {
var defaultProvider = this.addStandardFileSyncProvider(this.defaultServerURL());
// no providers saved, this means migrating from old system to new
// check if user is signed in
if(this.offline && localStorage.getItem("user")) {
var defaultProvider = this.addStandardFileSyncProvider(this.serverURL());
defaultProvider.syncToken = localStorage.getItem("syncToken");
// migrate old key structure to new
var mk = localStorage.getItem("mk");
if(mk) {
keyManager.addKey(this.SNKeyName, mk);
keyManager.addKey(SNKeyName, mk);
localStorage.removeItem("mk");
}
this.didMakeChangesToSyncProviders();
@@ -129,43 +143,3 @@ class SyncManager {
}
angular.module('app.frontend').service('syncManager', SyncManager);
class SyncProvider {
constructor(obj) {
this.encrypted = true;
_.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,
primary: this.primary,
keyName: this.keyName,
syncToken: this.syncToken
}
}
}

View File

@@ -0,0 +1,43 @@
class SyncProvider {
constructor(obj) {
this.encrypted = true;
_.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 isStandardNotesAccount() {
return this.keyName == SNKeyName;
}
get status() {
if(!this.enabled) {
return null;
}
if(this.primary) return "primary";
else return "secondary";
}
asJSON() {
return {
enabled: this.enabled,
url: this.url,
primary: this.primary,
keyName: this.keyName,
syncToken: this.syncToken
}
}
}

View File

@@ -1,10 +1,12 @@
class SyncPerformer {
class SyncRunner {
constructor(modelManager, dbManager, encryptionHelper, keyManager) {
constructor($rootScope, modelManager, dbManager, encryptionHelper, keyManager, Restangular) {
this.rootScope = $rootScope;
this.modelManager = modelManager;
this.dbManager = dbManager;
this.encryptionHelper = encryptionHelper;
this.keyManager = keyManager;
this.Restangular = Restangular;
}
setOnChangeProviderCallback(callback) {
@@ -15,9 +17,14 @@ class SyncPerformer {
this.onChangeProviderCallback(provider);
}
writeItemsToLocalStorage(items, callback) {
writeItemsToLocalStorage(items, offlineOnly, callback) {
var params = items.map(function(item) {
return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true)
var itemParams = new ItemParams(item, null);
itemParams = itemParams.paramsForLocalStorage();
if(offlineOnly) {
delete itemParams.dirty;
}
return itemParams;
}.bind(this));
this.dbManager.saveItems(params, callback);
@@ -32,7 +39,8 @@ class SyncPerformer {
}
syncOffline(items, callback) {
this.writeItemsToLocalStorage(items, function(responseItems){
console.log("Writing items offline", items);
this.writeItemsToLocalStorage(items, true, function(responseItems){
// delete anything needing to be deleted
for(var item of items) {
if(item.deleted) {
@@ -57,10 +65,6 @@ class SyncPerformer {
}
for(let provider of providers) {
if(!provider.enabled) {
continue;
}
provider.addPendingItems(allDirtyItems);
this.didMakeChangesToSyncProvider(provider);
@@ -100,13 +104,13 @@ class SyncPerformer {
provider.repeatOnCompletion = false;
}
console.log("Syncing with provider", provider, subItems);
console.log("Syncing with provider:", provider.url, "items:", subItems.length);
// 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);
var request = this.Restangular.oneUrl(provider.url, provider.url);
request.limit = 150;
request.items = _.map(subItems, function(item){
var itemParams = new ItemParams(item, provider.ek);
@@ -119,12 +123,12 @@ class SyncPerformer {
request.post().then(function(response) {
console.log("Sync completion", response);
console.log("Completed sync for provider:", provider.url, "Response:", response);
provider.syncToken = response.sync_token;
if(provider.primary) {
$rootScope.$broadcast("sync:updated_token", provider.syncToken);
this.rootScope.$broadcast("sync:updated_token", provider.syncToken);
// handle cursor token (more results waiting, perform another sync)
provider.cursorToken = response.cursor_token;
@@ -136,8 +140,8 @@ class SyncPerformer {
this.handleUnsavedItemsResponse(response.unsaved, provider)
this.writeItemsToLocalStorage(saved, null);
this.writeItemsToLocalStorage(retrieved, null);
this.writeItemsToLocalStorage(saved, false, null);
this.writeItemsToLocalStorage(retrieved, false, null);
}
provider.syncOpInProgress = false;
@@ -159,7 +163,7 @@ class SyncPerformer {
provider.syncOpInProgress = false;
if(provider.primary) {
this.writeItemsToLocalStorage(allItems, null);
this.writeItemsToLocalStorage(allItems, false, null);
}
if(callback) {
@@ -194,3 +198,5 @@ class SyncPerformer {
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
}
}
angular.module('app.frontend').service('syncRunner', SyncRunner);