Merge pull request #163 from standardnotes/desktopManager
Desktop manager to allow local backups
This commit is contained in:
@@ -55,6 +55,7 @@ angular.module('app.frontend')
|
|||||||
themeManager.activateInitialTheme();
|
themeManager.activateInitialTheme();
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
|
$rootScope.$broadcast("initial-data-loaded");
|
||||||
|
|
||||||
syncManager.sync(null);
|
syncManager.sync(null);
|
||||||
// refresh every 30s
|
// refresh every 30s
|
||||||
|
|||||||
59
app/assets/javascripts/app/services/desktopManager.js
Normal file
59
app/assets/javascripts/app/services/desktopManager.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// An interface used by the Desktop app to interact with SN
|
||||||
|
|
||||||
|
class DesktopManager {
|
||||||
|
|
||||||
|
constructor($rootScope, modelManager, authManager, passcodeManager) {
|
||||||
|
this.passcodeManager = passcodeManager;
|
||||||
|
this.modelManager = modelManager;
|
||||||
|
this.authManager = authManager;
|
||||||
|
this.$rootScope = $rootScope;
|
||||||
|
|
||||||
|
$rootScope.$on("initial-data-loaded", () => {
|
||||||
|
this.dataLoaded = true;
|
||||||
|
if(this.dataLoadHandler) {
|
||||||
|
this.dataLoadHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$rootScope.$on("major-data-change", () => {
|
||||||
|
if(this.majorDataChangeHandler) {
|
||||||
|
this.majorDataChangeHandler();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop_setInitialDataLoadHandler(handler) {
|
||||||
|
this.dataLoadHandler = handler;
|
||||||
|
if(this.dataLoaded) {
|
||||||
|
this.dataLoadHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop_requestBackupFile() {
|
||||||
|
var keys, authParams, protocolVersion;
|
||||||
|
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||||
|
keys = this.passcodeManager.keys();
|
||||||
|
authParams = this.passcodeManager.passcodeAuthParams();
|
||||||
|
protocolVersion = authParams.version;
|
||||||
|
} else {
|
||||||
|
keys = this.authManager.keys();
|
||||||
|
authParams = this.authManager.getAuthParams();
|
||||||
|
protocolVersion = this.authManager.protocolVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = this.modelManager.getAllItemsJSONData(
|
||||||
|
keys,
|
||||||
|
authParams,
|
||||||
|
protocolVersion,
|
||||||
|
true /* return null on empty */
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop_setMajorDataChangeHandler(handler) {
|
||||||
|
this.majorDataChangeHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('app.frontend').service('desktopManager', DesktopManager);
|
||||||
@@ -8,13 +8,17 @@ class AccountMenu {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
controller($scope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||||
'ngInject';
|
'ngInject';
|
||||||
|
|
||||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||||
$scope.user = authManager.user;
|
$scope.user = authManager.user;
|
||||||
$scope.server = syncManager.serverURL;
|
$scope.server = syncManager.serverURL;
|
||||||
|
|
||||||
|
$scope.encryptedBackupsAvailable = function() {
|
||||||
|
return authManager.user || passcodeManager.hasPasscode();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.syncStatus = syncManager.syncStatus;
|
$scope.syncStatus = syncManager.syncStatus;
|
||||||
|
|
||||||
$scope.encryptionKey = function() {
|
$scope.encryptionKey = function() {
|
||||||
@@ -153,6 +157,9 @@ class AccountMenu {
|
|||||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||||
block();
|
block();
|
||||||
}, true)
|
}, true)
|
||||||
|
|
||||||
|
// Allows desktop to make backup file
|
||||||
|
$rootScope.$broadcast("major-data-change");
|
||||||
} else {
|
} else {
|
||||||
modelManager.resetLocalMemory();
|
modelManager.resetLocalMemory();
|
||||||
storageManager.clearAllModels(function(){
|
storageManager.clearAllModels(function(){
|
||||||
@@ -174,7 +181,7 @@ class AccountMenu {
|
|||||||
|
|
||||||
/* Import/Export */
|
/* Import/Export */
|
||||||
|
|
||||||
$scope.archiveFormData = {encrypted: $scope.user ? true : false};
|
$scope.archiveFormData = {encrypted: $scope.encryptedBackupsAvailable() ? true : false};
|
||||||
$scope.user = authManager.user;
|
$scope.user = authManager.user;
|
||||||
|
|
||||||
$scope.submitImportPassword = function() {
|
$scope.submitImportPassword = function() {
|
||||||
@@ -361,8 +368,19 @@ class AccountMenu {
|
|||||||
|
|
||||||
$scope.downloadDataArchive = function() {
|
$scope.downloadDataArchive = function() {
|
||||||
// download in Standard File format
|
// download in Standard File format
|
||||||
var keys = $scope.archiveFormData.encrypted ? authManager.keys() : null;
|
var keys, authParams, protocolVersion;
|
||||||
var data = $scope.itemsData(keys);
|
if($scope.archiveFormData.encrypted) {
|
||||||
|
if(authManager.offline() && passcodeManager.hasPasscode()) {
|
||||||
|
keys = passcodeManager.keys();
|
||||||
|
authParams = passcodeManager.passcodeAuthParams();
|
||||||
|
protocolVersion = authParams.version;
|
||||||
|
} else {
|
||||||
|
keys = authManager.keys();
|
||||||
|
authParams = authManager.getAuthParams();
|
||||||
|
protocolVersion = authManager.protocolVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data = $scope.itemsData(keys, authParams, protocolVersion);
|
||||||
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||||
|
|
||||||
// download as zipped plain text files
|
// download as zipped plain text files
|
||||||
@@ -372,21 +390,10 @@ class AccountMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.itemsData = function(keys) {
|
$scope.itemsData = function(keys, authParams, protocolVersion) {
|
||||||
var items = _.map(modelManager.allItems, function(item){
|
let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||||
var itemParams = new ItemParams(item, keys, authManager.protocolVersion());
|
let blobData = new Blob([data], {type: 'text/json'});
|
||||||
return itemParams.paramsForExportFile();
|
return blobData;
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
var data = {items: items}
|
|
||||||
|
|
||||||
if(keys) {
|
|
||||||
// auth params are only needed when encrypted with a standard file key
|
|
||||||
data["auth_params"] = authManager.getAuthParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = new Blob([JSON.stringify(data, null, 2 /* pretty print */)], {type: 'text/json'});
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -527,6 +534,8 @@ class AccountMenu {
|
|||||||
|
|
||||||
if(offline) {
|
if(offline) {
|
||||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||||
|
// Allows desktop to make backup file
|
||||||
|
$rootScope.$broadcast("major-data-change");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -540,8 +549,12 @@ class AccountMenu {
|
|||||||
}
|
}
|
||||||
if(confirm(message)) {
|
if(confirm(message)) {
|
||||||
passcodeManager.clearPasscode();
|
passcodeManager.clearPasscode();
|
||||||
|
|
||||||
if(authManager.offline()) {
|
if(authManager.offline()) {
|
||||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||||
|
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
|
||||||
|
// we don't want to write unencrypted data to disk.
|
||||||
|
// $rootScope.$broadcast("major-data-change");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class ModelManager {
|
|||||||
|
|
||||||
// first loop should add and process items
|
// first loop should add and process items
|
||||||
for (var json_obj of items) {
|
for (var json_obj of items) {
|
||||||
if((!json_obj.content_type || !json_obj.content) && !json_obj.deleted) {
|
if((!json_obj.content_type || !json_obj.content) && !json_obj.deleted && !json_obj.errorDecrypting) {
|
||||||
// An item that is not deleted should never have empty content
|
// An item that is not deleted should never have empty content
|
||||||
console.error("Server response item is corrupt:", json_obj);
|
console.error("Server response item is corrupt:", json_obj);
|
||||||
continue;
|
continue;
|
||||||
@@ -364,6 +364,31 @@ class ModelManager {
|
|||||||
itemOne.setDirty(true);
|
itemOne.setDirty(true);
|
||||||
itemTwo.setDirty(true);
|
itemTwo.setDirty(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Archives
|
||||||
|
*/
|
||||||
|
|
||||||
|
getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
||||||
|
var items = _.map(this.allItems, (item) => {
|
||||||
|
var itemParams = new ItemParams(item, keys, protocolVersion);
|
||||||
|
return itemParams.paramsForExportFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(returnNullIfEmpty && items.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {items: items}
|
||||||
|
|
||||||
|
if(keys) {
|
||||||
|
// auth params are only needed when encrypted with a standard file key
|
||||||
|
data["auth_params"] = authParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
angular.module('app.frontend').service('modelManager', ModelManager);
|
angular.module('app.frontend').service('modelManager', ModelManager);
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ angular.module('app.frontend')
|
|||||||
return this._keys;
|
return this._keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.passcodeAuthParams = function() {
|
||||||
|
return JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
||||||
|
}
|
||||||
|
|
||||||
this.unlock = function(passcode, callback) {
|
this.unlock = function(passcode, callback) {
|
||||||
var params = JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
var params = this.passcodeAuthParams();
|
||||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
|
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
|
||||||
if(keys.pw !== params.hash) {
|
if(keys.pw !== params.hash) {
|
||||||
callback(false);
|
callback(false);
|
||||||
@@ -40,7 +44,7 @@ angular.module('app.frontend')
|
|||||||
this.setPasscode = function(passcode, callback) {
|
this.setPasscode = function(passcode, callback) {
|
||||||
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
||||||
var salt = Neeto.crypto.generateRandomKey(512);
|
var salt = Neeto.crypto.generateRandomKey(512);
|
||||||
var defaultParams = {pw_cost: cost, pw_salt: salt};
|
var defaultParams = {pw_cost: cost, pw_salt: salt, version: "002"};
|
||||||
|
|
||||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
|
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
|
||||||
defaultParams.hash = keys.pw;
|
defaultParams.hash = keys.pw;
|
||||||
|
|||||||
@@ -282,7 +282,8 @@ class SyncManager {
|
|||||||
this.handleItemsResponse(response.saved_items, omitFields);
|
this.handleItemsResponse(response.saved_items, omitFields);
|
||||||
|
|
||||||
// Create copies of items or alternate their uuids if neccessary
|
// Create copies of items or alternate their uuids if neccessary
|
||||||
this.handleUnsavedItemsResponse(response.unsaved)
|
var unsaved = response.unsaved;
|
||||||
|
this.handleUnsavedItemsResponse(unsaved)
|
||||||
|
|
||||||
this.writeItemsToLocalStorage(saved, false, null);
|
this.writeItemsToLocalStorage(saved, false, null);
|
||||||
|
|
||||||
@@ -306,6 +307,18 @@ class SyncManager {
|
|||||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||||
} else {
|
} else {
|
||||||
this.writeItemsToLocalStorage(this.allRetreivedItems, false, null);
|
this.writeItemsToLocalStorage(this.allRetreivedItems, false, null);
|
||||||
|
|
||||||
|
// The number of changed items that constitute a major change
|
||||||
|
// This is used by the desktop app to create backups
|
||||||
|
let majorDataChangeThreshold = 5;
|
||||||
|
if(
|
||||||
|
this.allRetreivedItems.length >= majorDataChangeThreshold ||
|
||||||
|
saved.length >= majorDataChangeThreshold ||
|
||||||
|
unsaved.length >= majorDataChangeThreshold
|
||||||
|
) {
|
||||||
|
this.$rootScope.$broadcast("major-data-change");
|
||||||
|
}
|
||||||
|
|
||||||
this.allRetreivedItems = [];
|
this.allRetreivedItems = [];
|
||||||
|
|
||||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||||
|
|||||||
@@ -141,15 +141,15 @@
|
|||||||
|
|
||||||
.mt-25{"ng-if" => "!importData.loading"}
|
.mt-25{"ng-if" => "!importData.loading"}
|
||||||
%h4 Data Archives
|
%h4 Data Archives
|
||||||
.mt-5{"ng-if" => "user"}
|
.mt-5{"ng-if" => "encryptedBackupsAvailable()"}
|
||||||
%label.normal.inline{"ng-if" => "user"}
|
%label.normal.inline
|
||||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
|
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
|
||||||
Encrypted
|
Encrypted
|
||||||
%label.normal.inline
|
%label.normal.inline
|
||||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
|
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
|
||||||
Decrypted
|
Decrypted
|
||||||
|
|
||||||
%a.block.mt-5{"ng-click" => "downloadDataArchive()", "ng-class" => "{'mt-5' : !user}"} Export Data Archive
|
%a.block.mt-5{"ng-click" => "downloadDataArchive()", "ng-class" => "{'mt-5' : !user}"} Download Data Archive
|
||||||
|
|
||||||
%label.block.mt-5
|
%label.block.mt-5
|
||||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||||
|
|||||||
Reference in New Issue
Block a user