diff --git a/app/scripts/collections/file-info-collection.js b/app/scripts/collections/file-info-collection.js index 45e5b505..dd8f555e 100644 --- a/app/scripts/collections/file-info-collection.js +++ b/app/scripts/collections/file-info-collection.js @@ -31,6 +31,10 @@ var FileInfoCollection = Backbone.Collection.extend({ (fi.get('name') || '') === (name || '') && (fi.get('path') || '') === (path || ''); }); + }, + + getByName: function() { + return this.find(function(file) { return file.get('name') === name; }); } }); diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index bdbc0fb4..2346a518 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -234,7 +234,7 @@ var AppModel = Backbone.Model.extend({ var name; for (var i = 0; ; i++) { name = 'New' + (i || ''); - if (!this.files.getByName(name)) { + if (!this.files.getByName(name) && !this.fileInfos.getByName(name)) { break; } } @@ -317,6 +317,12 @@ var AppModel = Backbone.Model.extend({ if (that.files.get(file.id)) { return callback('Duplicate file id'); } + if (fileInfo && fileInfo.get('modified')) { + if (fileInfo.get('editState')) { + file.setLocalEditState(fileInfo.get('editState')); + } + file.setModified(); + } var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid(); if (updateCacheOnSuccess && params.storage !== 'file') { Storage.cache.save(cacheId, params.fileData, function(err) { @@ -341,7 +347,7 @@ var AppModel = Backbone.Model.extend({ storage: file.get('storage'), path: file.get('path'), modified: file.get('modified'), - editState: null, + editState: file.getLocalEditState(), rev: rev, pullDate: dt, openDate: dt @@ -361,7 +367,10 @@ var AppModel = Backbone.Model.extend({ if (file.get('syncing')) { return callback('Sync in progress'); } - // todo: save to cache + var complete = function(err) { + // TODO: save file info + callback(err); + }; var fileInfo = this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path')); if (!fileInfo) { var dt = new Date(); @@ -378,66 +387,77 @@ var AppModel = Backbone.Model.extend({ }); } var storage = Storage[options.storage || file.get('storage')]; + var path = options.path || file.get('path'); if (!storage) { if (!file.get('modified')) { - return callback(); + return complete(); } file.getData(function(data, err) { - if (err) { return callback(err); } + if (err) { return complete(err); } Storage.cache.save(fileInfo.id, data, function(err) { - callback(err); + complete(err); }); }); } else { var maxLoadLoops = 3, loadLoops = 0; var loadFromStorageAndMerge = function() { if (++loadLoops === maxLoadLoops) { - return callback('Too many load attempts, please try again later'); + return complete('Too many load attempts, please try again later'); } - storage.load(file.get('path'), function(err, data, stat) { - if (err) { return callback(err); } - file.merge(data, function(err) { - if (err) { return callback(err); } + storage.load(path, function(err, data, stat) { + if (err) { return complete(err); } + file.mergeOrUpdate(data, function(err) { + if (err) { return complete(err); } if (stat && stat.rev) { fileInfo.set('rev', stat.rev); } if (file.get('modified')) { - saveToStorage(); + saveToCacheAndStorage(); } else { - callback(); + complete(); } }); }); }; - var saveToStorage = function() { + var saveToCacheAndStorage = function() { file.getData(function(data, err) { - if (err) { return callback(err); } - storage.save(file.get('path'), data, function(err) { - if (err && err.revConflict) { - loadFromStorageAndMerge(); - } else if (err) { - callback(err); - } else { - if (storage === Storage.file) { - Storage.cache.remove(fileInfo.id); - } - callback(); - } - }, fileInfo.get('rev')); + if (err) { return complete(err); } + if (storage === Storage.file) { + saveToStorage(data); + } else { + Storage.cache.save(fileInfo.id, data, function (err) { + if (err) { return complete(err); } + saveToStorage(data); + }); + } }); }; + var saveToStorage = function(data) { + storage.save(path, data, function(err) { + if (err && err.revConflict) { + loadFromStorageAndMerge(); + } else if (err) { + complete(err); + } else { + if (storage === Storage.file) { + Storage.cache.remove(fileInfo.id); + } + complete(); + } + }, fileInfo.get('rev')); + }; if (options.reload) { loadFromStorageAndMerge(); } else if (storage === Storage.file) { if (file.get('modified')) { - saveToStorage(); + saveToCacheAndStorage(); } else { - callback(); + complete(); } } else { - storage.stat(file.get('path'), function (err, stat) { + storage.stat(path, function (err, stat) { if (stat.rev === fileInfo.get('rev')) { - saveToStorage(); + saveToCacheAndStorage(); } else { loadFromStorageAndMerge(); } diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 5471c760..2e818080 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -24,7 +24,8 @@ var FileModel = Backbone.Model.extend({ oldKeyFileName: '', passwordChanged: false, keyFileChanged: false, - syncing: false + syncing: false, + syncError: null }, db: null, @@ -155,18 +156,39 @@ var FileModel = Backbone.Model.extend({ this.trigger('reload', this); }, - merge: function(fileData, callback) { + mergeOrUpdate: function(fileData, callback) { kdbxweb.Kdbx.load(fileData, this.db.credentials, (function(remoteDb, err) { if (err) { console.error('Error opening file to merge', err.code, err.message, err); } else { - this.db.merge(remoteDb); - this.reload(); + if (this.get('modified')) { + try { + this.db.merge(remoteDb); + } catch (e) { + console.error('File merge error', e); + return callback(e); + } + } else { + this.db = remoteDb; + this.reload(); + } } callback(err); }).bind(this)); }, + getLocalEditState: function() { + return this.db.getLocalEditState(); + }, + + setLocalEditState: function(editState) { + this.db.setLocalEditState(editState); + }, + + removeLocalEditState: function() { + this.db.removeLocalEditState(); + }, + close: function() { this.set({ keyFileName: '', @@ -244,8 +266,20 @@ var FileModel = Backbone.Model.extend({ this.db.saveXml(cb); }, - saved: function(path, storage) { - this.set({ path: path || '', storage: storage || null, modified: false, created: false, syncing: false }); + setSyncProgress: function() { + this.set({ syncing: true }); + }, + + setSyncComplete: function(path, storage, error) { + var modified = this.get('modified') && !!error; + this.set({ + created: false, + path: path || this.get('path'), + storage: storage || this.get('storage'), + modified: modified, + syncing: false, + syncError: error + }); this.setOpenFile({ passwordLength: this.get('passwordLength') }); this.forEachEntry({}, function(entry) { entry.unsaved = false; diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index a39aba2b..acd25eb2 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -322,10 +322,6 @@ var AppView = Backbone.View.extend({ } }, - showVisualLock: function() { - // TODO: remove cases which lead to this - }, - saveAndLock: function(autoInit) { // TODO: move to file manager var pendingCallbacks = 0, diff --git a/app/scripts/views/settings/settings-file-view.js b/app/scripts/views/settings/settings-file-view.js index bfbb75aa..a0296bcf 100644 --- a/app/scripts/views/settings/settings-file-view.js +++ b/app/scripts/views/settings/settings-file-view.js @@ -137,28 +137,36 @@ var SettingsAboutView = Backbone.View.extend({ if (skipValidation !== true && !this.validatePassword(this.saveToFile.bind(this, true))) { return; } + var fileName = this.model.get('name') + '.kdbx'; var that = this; - this.model.getData(function(data) { - var fileName = that.model.get('name') + '.kdbx'; - if (Launcher) { - Launcher.getSaveFileName(fileName, function (path) { - if (path) { - Storage.file.save(path, data, function(err) { - if (err) { - Alerts.error({ - header: 'Save error', - body: 'Error saving to file ' + path + ': \n' + err - }); - } - }); - } - }); - } else { - var blob = new Blob([data], {type: 'application/octet-stream'}); - FileSaver.saveAs(blob, fileName); - that.passwordChanged = false; - } - }); + if (Launcher && !this.model.get('storage')) { + Launcher.getSaveFileName(fileName, function (path) { + if (path) { + that.save({storage: 'file', path: path}); + } + }); + } else { + this.model.getData(function (data) { + if (Launcher) { + Launcher.getSaveFileName(fileName, function (path) { + if (path) { + Storage.file.save(path, data, function (err) { + if (err) { + Alerts.error({ + header: 'Save error', + body: 'Error saving to file ' + path + ': \n' + err + }); + } + }); + } + }); + } else { + var blob = new Blob([data], {type: 'application/octet-stream'}); + FileSaver.saveAs(blob, fileName); + that.passwordChanged = false; + } + }); + } }, exportAsXml: function() {