2017-01-31 07:50:28 +01:00
|
|
|
const Backbone = require('backbone');
|
|
|
|
const AppSettingsModel = require('./app-settings-model');
|
|
|
|
const MenuModel = require('./menu/menu-model');
|
|
|
|
const EntryModel = require('./entry-model');
|
|
|
|
const GroupModel = require('./group-model');
|
|
|
|
const FileCollection = require('../collections/file-collection');
|
|
|
|
const EntryCollection = require('../collections/entry-collection');
|
|
|
|
const FileInfoCollection = require('../collections/file-info-collection');
|
|
|
|
const FileModel = require('./file-model');
|
|
|
|
const FileInfoModel = require('./file-info-model');
|
|
|
|
const Storage = require('../storage');
|
|
|
|
const Timeouts = require('../const/timeouts');
|
|
|
|
const IdGenerator = require('../util/id-generator');
|
|
|
|
const Logger = require('../util/logger');
|
|
|
|
const FeatureDetector = require('../util/feature-detector');
|
|
|
|
const Format = require('../util/format');
|
|
|
|
const UrlUtil = require('../util/url-util');
|
|
|
|
const AutoType = require('../auto-type');
|
2017-02-05 22:01:46 +01:00
|
|
|
const Launcher = require('../comp/launcher');
|
2017-12-16 13:17:01 +01:00
|
|
|
const RuntimeInfo = require('../comp/runtime-info');
|
2017-05-16 21:26:25 +02:00
|
|
|
const PluginManager = require('../plugins/plugin-manager');
|
2016-01-16 21:32:50 +01:00
|
|
|
|
2016-01-16 15:19:33 +01:00
|
|
|
require('../mixins/protected-value-ex');
|
2015-10-17 23:49:24 +02:00
|
|
|
|
2017-01-31 07:50:28 +01:00
|
|
|
const AppModel = Backbone.Model.extend({
|
2015-10-17 23:49:24 +02:00
|
|
|
defaults: {},
|
|
|
|
|
|
|
|
initialize: function() {
|
|
|
|
this.tags = [];
|
|
|
|
this.files = new FileCollection();
|
2017-02-05 21:28:43 +01:00
|
|
|
this.fileInfos = FileInfoCollection.instance;
|
2015-10-17 23:49:24 +02:00
|
|
|
this.menu = new MenuModel();
|
|
|
|
this.filter = {};
|
|
|
|
this.sort = 'title';
|
|
|
|
this.settings = AppSettingsModel.instance;
|
2015-12-12 12:04:02 +01:00
|
|
|
this.activeEntryId = null;
|
2017-12-16 13:13:59 +01:00
|
|
|
this.isBeta = RuntimeInfo.beta;
|
2018-12-30 20:03:02 +01:00
|
|
|
this.advancedSearch = null;
|
2015-10-17 23:49:24 +02:00
|
|
|
|
|
|
|
this.listenTo(Backbone, 'refresh', this.refresh);
|
|
|
|
this.listenTo(Backbone, 'set-filter', this.setFilter);
|
|
|
|
this.listenTo(Backbone, 'add-filter', this.addFilter);
|
|
|
|
this.listenTo(Backbone, 'set-sort', this.setSort);
|
2015-11-08 22:25:00 +01:00
|
|
|
this.listenTo(Backbone, 'empty-trash', this.emptyTrash);
|
2016-08-21 19:39:02 +02:00
|
|
|
this.listenTo(Backbone, 'select-entry', this.selectEntry);
|
2018-12-28 13:16:19 +01:00
|
|
|
this.listenTo(Backbone, 'unset-keyfile', this.unsetKeyFile);
|
2015-12-12 09:53:50 +01:00
|
|
|
|
|
|
|
this.appLogger = new Logger('app');
|
2017-05-22 21:59:01 +02:00
|
|
|
|
|
|
|
AppModel.instance = this;
|
2016-09-20 22:30:19 +02:00
|
|
|
},
|
2016-07-24 22:57:12 +02:00
|
|
|
|
2016-09-20 22:30:19 +02:00
|
|
|
prepare: function() {
|
2016-07-24 22:57:12 +02:00
|
|
|
AutoType.init(this);
|
2016-09-20 22:30:19 +02:00
|
|
|
_.forEach(Storage, prv => prv.init());
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2017-05-16 21:26:25 +02:00
|
|
|
loadConfig: function(configLocation) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2017-08-31 19:08:39 +02:00
|
|
|
this.ensureCanLoadConfig(configLocation);
|
2017-05-16 21:26:25 +02:00
|
|
|
this.appLogger.debug('Loading config from', configLocation);
|
|
|
|
const ts = this.appLogger.ts();
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.open('GET', configLocation);
|
|
|
|
xhr.responseType = 'json';
|
|
|
|
xhr.send();
|
|
|
|
xhr.addEventListener('load', () => {
|
|
|
|
let response = xhr.response;
|
|
|
|
if (!response) {
|
|
|
|
const errorDesc = xhr.statusText === 'OK' ? 'Malformed JSON' : xhr.statusText;
|
|
|
|
this.appLogger.error('Error loading app config', errorDesc);
|
|
|
|
return reject('Error loading app config');
|
2016-09-21 18:44:41 +02:00
|
|
|
}
|
2017-05-16 21:26:25 +02:00
|
|
|
if (typeof response === 'string') {
|
|
|
|
try {
|
|
|
|
response = JSON.parse(response);
|
|
|
|
} catch (e) {
|
|
|
|
this.appLogger.error('Error parsing response', e, response);
|
|
|
|
return reject('Error parsing response');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!response.settings) {
|
|
|
|
this.appLogger.error('Invalid app config, no settings section', response);
|
|
|
|
return reject('Invalid app config, no settings section');
|
|
|
|
}
|
|
|
|
this.appLogger.info('Loaded app config from', configLocation, this.appLogger.ts(ts));
|
|
|
|
resolve(response);
|
|
|
|
});
|
|
|
|
xhr.addEventListener('error', () => {
|
|
|
|
this.appLogger.error('Error loading app config', xhr.statusText, xhr.status);
|
|
|
|
reject('Error loading app config');
|
|
|
|
});
|
|
|
|
}).then(config => {
|
|
|
|
return this.applyUserConfig(config);
|
2016-06-16 19:00:24 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-08-31 19:08:39 +02:00
|
|
|
ensureCanLoadConfig(url) {
|
|
|
|
if (!FeatureDetector.isSelfHosted) {
|
|
|
|
throw 'Configs are supported only in self-hosted installations';
|
|
|
|
}
|
|
|
|
const link = document.createElement('a');
|
|
|
|
link.href = url;
|
|
|
|
const isExternal = link.host && link.host !== location.host;
|
|
|
|
if (isExternal) {
|
|
|
|
throw 'Loading config from this location is not allowed';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-07-30 12:38:37 +02:00
|
|
|
applyUserConfig(config) {
|
|
|
|
this.settings.set(config.settings);
|
2016-07-31 09:47:56 +02:00
|
|
|
if (config.files) {
|
2017-02-07 19:17:34 +01:00
|
|
|
if (config.showOnlyFilesFromConfig) {
|
|
|
|
this.fileInfos.reset();
|
|
|
|
}
|
2016-07-31 09:47:56 +02:00
|
|
|
config.files
|
|
|
|
.filter(file => file && file.storage && file.name && file.path &&
|
|
|
|
!this.fileInfos.getMatch(file.storage, file.name, file.path))
|
|
|
|
.map(file => new FileInfoModel({
|
2016-07-30 12:38:37 +02:00
|
|
|
id: IdGenerator.uuid(),
|
2016-07-31 09:47:56 +02:00
|
|
|
name: file.name,
|
|
|
|
storage: file.storage,
|
|
|
|
path: file.path,
|
|
|
|
opts: file.options
|
|
|
|
}))
|
|
|
|
.reverse()
|
|
|
|
.forEach(fi => this.fileInfos.unshift(fi));
|
2016-07-30 12:38:37 +02:00
|
|
|
}
|
2017-05-16 21:26:25 +02:00
|
|
|
if (config.plugins) {
|
2018-12-20 20:58:10 +01:00
|
|
|
const pluginsPromises = config.plugins
|
|
|
|
.map(plugin => PluginManager.installIfNew(plugin.url, plugin.manifest, true));
|
|
|
|
return Promise.all(pluginsPromises).then(() => {
|
|
|
|
this.settings.set(config.settings);
|
|
|
|
});
|
2017-05-16 21:26:25 +02:00
|
|
|
}
|
2018-12-30 20:03:02 +01:00
|
|
|
if (config.advancedSearch) {
|
|
|
|
this.advancedSearch = config.advancedSearch;
|
|
|
|
this.addFilter({advanced: this.advancedSearch});
|
|
|
|
}
|
2016-07-30 12:38:37 +02:00
|
|
|
},
|
|
|
|
|
2015-10-17 23:49:24 +02:00
|
|
|
addFile: function(file) {
|
2016-06-05 16:49:00 +02:00
|
|
|
if (this.files.get(file.id)) {
|
2015-12-02 21:50:31 +01:00
|
|
|
return false;
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
this.files.add(file);
|
2015-12-02 21:50:31 +01:00
|
|
|
file.get('groups').forEach(function (group) {
|
|
|
|
this.menu.groupsSection.addItem(group);
|
|
|
|
}, this);
|
2015-12-07 20:07:56 +01:00
|
|
|
this._addTags(file);
|
2015-10-17 23:49:24 +02:00
|
|
|
this._tagsChanged();
|
|
|
|
this.menu.filesSection.addItem({
|
|
|
|
icon: 'lock',
|
|
|
|
title: file.get('name'),
|
|
|
|
page: 'file',
|
|
|
|
file: file
|
|
|
|
});
|
|
|
|
this.refresh();
|
2015-12-05 14:04:09 +01:00
|
|
|
this.listenTo(file, 'reload', this.reloadFile);
|
2015-12-02 21:50:31 +01:00
|
|
|
return true;
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2015-12-05 14:04:09 +01:00
|
|
|
reloadFile: function(file) {
|
2016-06-05 16:49:00 +02:00
|
|
|
this.menu.groupsSection.replaceByFile(file, file.get('groups').first());
|
2015-12-05 14:04:09 +01:00
|
|
|
this.updateTags();
|
|
|
|
},
|
|
|
|
|
2015-12-07 20:07:56 +01:00
|
|
|
_addTags: function(file) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const tagsHash = {};
|
2016-07-17 13:30:38 +02:00
|
|
|
this.tags.forEach(tag => {
|
2015-10-17 23:49:24 +02:00
|
|
|
tagsHash[tag.toLowerCase()] = true;
|
|
|
|
});
|
2016-07-17 13:30:38 +02:00
|
|
|
file.forEachEntry({}, entry => {
|
|
|
|
_.forEach(entry.tags, tag => {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (!tagsHash[tag.toLowerCase()]) {
|
|
|
|
tagsHash[tag.toLowerCase()] = true;
|
2016-07-17 13:30:38 +02:00
|
|
|
this.tags.push(tag);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
2015-12-07 20:07:56 +01:00
|
|
|
});
|
|
|
|
});
|
2015-10-17 23:49:24 +02:00
|
|
|
this.tags.sort();
|
|
|
|
},
|
|
|
|
|
|
|
|
_tagsChanged: function() {
|
|
|
|
if (this.tags.length) {
|
|
|
|
this.menu.tagsSection.set('scrollable', true);
|
2016-07-17 13:30:38 +02:00
|
|
|
this.menu.tagsSection.setItems(this.tags.map(tag => {
|
2016-04-17 22:02:39 +02:00
|
|
|
return {title: tag, icon: 'tag', filterKey: 'tag', filterValue: tag, editable: true};
|
2015-10-17 23:49:24 +02:00
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
this.menu.tagsSection.set('scrollable', false);
|
|
|
|
this.menu.tagsSection.removeAllItems();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
updateTags: function() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const oldTags = this.tags.slice();
|
2015-10-17 23:49:24 +02:00
|
|
|
this.tags.splice(0, this.tags.length);
|
|
|
|
this.files.forEach(function(file) {
|
2015-12-07 20:07:56 +01:00
|
|
|
this._addTags(file);
|
2015-10-17 23:49:24 +02:00
|
|
|
}, this);
|
|
|
|
if (!_.isEqual(oldTags, this.tags)) {
|
|
|
|
this._tagsChanged();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-04-17 22:02:39 +02:00
|
|
|
renameTag: function(from, to) {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.files.forEach(file => file.renameTag(from, to));
|
2016-04-17 22:02:39 +02:00
|
|
|
this.updateTags();
|
|
|
|
},
|
|
|
|
|
2015-10-17 23:49:24 +02:00
|
|
|
closeAllFiles: function() {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.files.each(file => {
|
2016-02-06 11:59:57 +01:00
|
|
|
file.close();
|
2016-07-17 13:30:38 +02:00
|
|
|
this.fileClosed(file);
|
2016-02-06 11:59:57 +01:00
|
|
|
});
|
2015-10-17 23:49:24 +02:00
|
|
|
this.files.reset();
|
|
|
|
this.menu.groupsSection.removeAllItems();
|
|
|
|
this.menu.tagsSection.set('scrollable', false);
|
|
|
|
this.menu.tagsSection.removeAllItems();
|
|
|
|
this.menu.filesSection.removeAllItems();
|
|
|
|
this.tags.splice(0, this.tags.length);
|
2015-12-12 12:04:02 +01:00
|
|
|
this.filter = {};
|
2016-07-16 12:01:19 +02:00
|
|
|
this.menu.select({ item: this.menu.allItemsItem });
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2015-11-07 21:37:54 +01:00
|
|
|
closeFile: function(file) {
|
2016-02-06 11:59:57 +01:00
|
|
|
file.close();
|
|
|
|
this.fileClosed(file);
|
2015-11-07 21:37:54 +01:00
|
|
|
this.files.remove(file);
|
2015-12-13 12:19:55 +01:00
|
|
|
this.updateTags();
|
2015-11-07 21:37:54 +01:00
|
|
|
this.menu.groupsSection.removeByFile(file);
|
|
|
|
this.menu.filesSection.removeByFile(file);
|
2016-06-05 16:49:00 +02:00
|
|
|
this.menu.select({ item: this.menu.allItemsSection.get('items').first() });
|
2015-11-07 21:37:54 +01:00
|
|
|
},
|
|
|
|
|
2015-11-08 22:25:00 +01:00
|
|
|
emptyTrash: function() {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.files.forEach(file => file.emptyTrash());
|
2015-11-08 22:25:00 +01:00
|
|
|
this.refresh();
|
|
|
|
},
|
|
|
|
|
2015-10-17 23:49:24 +02:00
|
|
|
setFilter: function(filter) {
|
|
|
|
this.filter = filter;
|
2015-11-06 21:42:37 +01:00
|
|
|
this.filter.subGroups = this.settings.get('expandGroups');
|
2017-01-31 07:50:28 +01:00
|
|
|
const entries = this.getEntries();
|
2015-12-12 12:04:02 +01:00
|
|
|
if (!this.activeEntryId || !entries.get(this.activeEntryId)) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const firstEntry = entries.first();
|
2015-12-12 12:04:02 +01:00
|
|
|
this.activeEntryId = firstEntry ? firstEntry.id : null;
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
Backbone.trigger('filter', { filter: this.filter, sort: this.sort, entries: entries });
|
2016-08-21 19:39:02 +02:00
|
|
|
Backbone.trigger('entry-selected', entries.get(this.activeEntryId));
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
refresh: function() {
|
|
|
|
this.setFilter(this.filter);
|
|
|
|
},
|
|
|
|
|
2016-08-21 19:39:02 +02:00
|
|
|
selectEntry: function(entry) {
|
|
|
|
this.activeEntryId = entry.id;
|
|
|
|
this.refresh();
|
|
|
|
},
|
|
|
|
|
2015-10-17 23:49:24 +02:00
|
|
|
addFilter: function(filter) {
|
|
|
|
this.setFilter(_.extend(this.filter, filter));
|
|
|
|
},
|
|
|
|
|
|
|
|
setSort: function(sort) {
|
|
|
|
this.sort = sort;
|
|
|
|
this.setFilter(this.filter);
|
|
|
|
},
|
|
|
|
|
|
|
|
getEntries: function() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const entries = this.getEntriesByFilter(this.filter);
|
2015-10-17 23:49:24 +02:00
|
|
|
entries.sortEntries(this.sort);
|
2016-07-31 20:38:08 +02:00
|
|
|
if (this.filter.trash) {
|
2015-11-08 22:25:00 +01:00
|
|
|
this.addTrashGroups(entries);
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
return entries;
|
|
|
|
},
|
|
|
|
|
2016-07-24 22:57:12 +02:00
|
|
|
getEntriesByFilter: function(filter) {
|
2016-07-31 20:38:08 +02:00
|
|
|
filter = this.prepareFilter(filter);
|
2017-01-31 07:50:28 +01:00
|
|
|
const entries = new EntryCollection();
|
2016-07-24 22:57:12 +02:00
|
|
|
this.files.forEach(file => {
|
|
|
|
file.forEachEntry(filter, entry => entries.push(entry));
|
|
|
|
});
|
|
|
|
return entries;
|
|
|
|
},
|
|
|
|
|
2015-11-08 22:25:00 +01:00
|
|
|
addTrashGroups: function(collection) {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.files.forEach(file => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const trashGroup = file.getTrashGroup();
|
2015-11-08 22:25:00 +01:00
|
|
|
if (trashGroup) {
|
2016-07-17 13:30:38 +02:00
|
|
|
trashGroup.getOwnSubGroups().forEach(group => {
|
2015-11-08 22:25:00 +01:00
|
|
|
collection.unshift(GroupModel.fromGroup(group, file, trashGroup));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2016-07-31 20:38:08 +02:00
|
|
|
prepareFilter: function(filter) {
|
|
|
|
filter = _.clone(filter);
|
2015-10-17 23:49:24 +02:00
|
|
|
if (filter.text) {
|
|
|
|
filter.textLower = filter.text.toLowerCase();
|
|
|
|
}
|
|
|
|
if (filter.tag) {
|
|
|
|
filter.tagLower = filter.tag.toLowerCase();
|
|
|
|
}
|
|
|
|
return filter;
|
|
|
|
},
|
|
|
|
|
2015-10-31 20:09:32 +01:00
|
|
|
getFirstSelectedGroup: function() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const selGroupId = this.filter.group;
|
|
|
|
let file,
|
|
|
|
group;
|
2015-10-17 23:49:24 +02:00
|
|
|
if (selGroupId) {
|
2017-01-29 19:34:47 +01:00
|
|
|
this.files.some(f => {
|
|
|
|
file = f;
|
2015-10-17 23:49:24 +02:00
|
|
|
group = f.getGroup(selGroupId);
|
2017-01-29 19:34:47 +01:00
|
|
|
return group;
|
2016-07-17 13:30:38 +02:00
|
|
|
});
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
if (!group) {
|
|
|
|
file = this.files.first();
|
|
|
|
group = file.get('groups').first();
|
|
|
|
}
|
2015-10-31 20:09:32 +01:00
|
|
|
return { group: group, file: file };
|
|
|
|
},
|
|
|
|
|
2016-02-28 12:16:05 +01:00
|
|
|
completeUserNames: function(part) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const userNames = {};
|
2016-07-17 13:30:38 +02:00
|
|
|
this.files.forEach(file => {
|
|
|
|
file.forEachEntry({ text: part, textLower: part.toLowerCase(), advanced: { user: true } }, entry => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const userName = entry.user;
|
2016-02-28 12:16:05 +01:00
|
|
|
if (userName) {
|
|
|
|
userNames[userName] = (userNames[userName] || 0) + 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2017-01-31 07:50:28 +01:00
|
|
|
const matches = _.pairs(userNames);
|
2016-07-17 13:30:38 +02:00
|
|
|
matches.sort((x, y) => y[1] - x[1]);
|
2017-01-31 07:50:28 +01:00
|
|
|
const maxResults = 5;
|
2016-02-28 12:16:05 +01:00
|
|
|
if (matches.length > maxResults) {
|
|
|
|
matches.length = maxResults;
|
|
|
|
}
|
2016-07-17 13:30:38 +02:00
|
|
|
return matches.map(m => m[0]);
|
2016-02-28 12:16:05 +01:00
|
|
|
},
|
|
|
|
|
2017-05-02 21:22:08 +02:00
|
|
|
getEntryTemplates: function() {
|
|
|
|
const entryTemplates = [];
|
|
|
|
this.files.forEach(file => {
|
|
|
|
file.forEachEntryTemplate(entry => {
|
|
|
|
entryTemplates.push({ file, entry });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return entryTemplates;
|
|
|
|
},
|
|
|
|
|
|
|
|
createNewEntry: function(args) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const sel = this.getFirstSelectedGroup();
|
2017-05-02 21:22:08 +02:00
|
|
|
if (args && args.template) {
|
|
|
|
if (sel.file !== args.template.file) {
|
|
|
|
sel.file = args.template.file;
|
|
|
|
sel.group = args.template.file.get('groups').first();
|
|
|
|
}
|
|
|
|
const templateEntry = args.template.entry;
|
|
|
|
const newEntry = EntryModel.newEntry(sel.group, sel.file);
|
|
|
|
newEntry.copyFromTemplate(templateEntry);
|
|
|
|
return newEntry;
|
|
|
|
} else {
|
|
|
|
return EntryModel.newEntry(sel.group, sel.file);
|
|
|
|
}
|
2015-10-31 20:09:32 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
createNewGroup: function() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const sel = this.getFirstSelectedGroup();
|
2015-10-31 20:09:32 +01:00
|
|
|
return GroupModel.newGroup(sel.group, sel.file);
|
2015-12-06 21:32:41 +01:00
|
|
|
},
|
|
|
|
|
2017-05-03 20:44:16 +02:00
|
|
|
createNewTemplateEntry: function() {
|
|
|
|
const file = this.getFirstSelectedGroup().file;
|
|
|
|
const group = file.getEntryTemplatesGroup() || file.createEntryTemplatesGroup();
|
|
|
|
return EntryModel.newEntry(group, file);
|
|
|
|
},
|
|
|
|
|
2015-12-06 21:32:41 +01:00
|
|
|
createDemoFile: function() {
|
|
|
|
if (!this.files.getByName('Demo')) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const demoFile = new FileModel({ id: IdGenerator.uuid() });
|
2016-07-17 13:30:38 +02:00
|
|
|
demoFile.openDemo(() => {
|
|
|
|
this.addFile(demoFile);
|
2015-12-06 21:32:41 +01:00
|
|
|
});
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
createNewFile: function() {
|
2017-01-31 07:50:28 +01:00
|
|
|
let name;
|
|
|
|
for (let i = 0; ; i++) {
|
2015-12-06 21:32:41 +01:00
|
|
|
name = 'New' + (i || '');
|
2015-12-10 22:31:47 +01:00
|
|
|
if (!this.files.getByName(name) && !this.fileInfos.getByName(name)) {
|
2015-12-06 21:32:41 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const newFile = new FileModel({ id: IdGenerator.uuid() });
|
2015-12-06 21:32:41 +01:00
|
|
|
newFile.create(name);
|
|
|
|
this.addFile(newFile);
|
|
|
|
},
|
|
|
|
|
|
|
|
openFile: function(params, callback) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('open', params.name);
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('File open request');
|
2017-01-31 07:50:28 +01:00
|
|
|
const fileInfo = params.id ? this.fileInfos.get(params.id) : this.fileInfos.getMatch(params.storage, params.name, params.path);
|
2016-03-12 21:08:49 +01:00
|
|
|
if (!params.opts && fileInfo && fileInfo.get('opts')) {
|
|
|
|
params.opts = fileInfo.get('opts');
|
|
|
|
}
|
2015-12-08 06:05:57 +01:00
|
|
|
if (fileInfo && fileInfo.get('modified')) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from cache because it is modified');
|
2016-07-17 13:30:38 +02:00
|
|
|
this.openFileFromCache(params, (err, file) => {
|
2016-03-19 10:27:26 +01:00
|
|
|
if (!err && file) {
|
|
|
|
logger.info('Sync just opened modified file');
|
2016-09-02 18:39:58 +02:00
|
|
|
_.defer(() => this.syncFile(file));
|
2016-03-19 10:27:26 +01:00
|
|
|
}
|
|
|
|
callback(err);
|
|
|
|
}, fileInfo);
|
2015-12-07 20:07:56 +01:00
|
|
|
} else if (params.fileData) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from supplied content');
|
2017-01-31 07:50:28 +01:00
|
|
|
const needSaveToCache = params.storage !== 'file';
|
2016-06-05 13:48:23 +02:00
|
|
|
this.openFileWithData(params, callback, fileInfo, params.fileData, needSaveToCache);
|
2015-12-12 16:43:43 +01:00
|
|
|
} else if (!params.storage) {
|
|
|
|
logger.info('Open file from cache as main storage');
|
|
|
|
this.openFileFromCache(params, callback, fileInfo);
|
2016-09-18 08:41:50 +02:00
|
|
|
} else if (fileInfo && fileInfo.get('openDate') && fileInfo.get('rev') === params.rev && fileInfo.get('storage') !== 'file') {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from cache because it is latest');
|
2015-12-07 20:07:56 +01:00
|
|
|
this.openFileFromCache(params, callback, fileInfo);
|
2016-09-18 08:41:50 +02:00
|
|
|
} else if (!fileInfo || !fileInfo.get('openDate') || params.storage === 'file') {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from storage', params.storage);
|
2017-01-31 07:50:28 +01:00
|
|
|
const storage = Storage[params.storage];
|
|
|
|
const storageLoad = () => {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Load from storage');
|
2016-07-17 13:30:38 +02:00
|
|
|
storage.load(params.path, params.opts, (err, data, stat) => {
|
2015-12-08 20:18:35 +01:00
|
|
|
if (err) {
|
2016-09-18 08:41:50 +02:00
|
|
|
if (fileInfo && fileInfo.get('openDate')) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from cache because of storage load error', err);
|
2016-09-02 18:39:58 +02:00
|
|
|
this.openFileFromCache(params, callback, fileInfo);
|
2015-12-08 20:18:35 +01:00
|
|
|
} else {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Storage load error', err);
|
2015-12-08 20:18:35 +01:00
|
|
|
callback(err);
|
|
|
|
}
|
|
|
|
} else {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Open file from content loaded from storage');
|
2015-12-08 20:18:35 +01:00
|
|
|
params.fileData = data;
|
|
|
|
params.rev = stat && stat.rev || null;
|
2017-01-31 07:50:28 +01:00
|
|
|
const needSaveToCache = storage.name !== 'file';
|
2016-09-02 18:39:58 +02:00
|
|
|
this.openFileWithData(params, callback, fileInfo, data, needSaveToCache);
|
2015-12-08 20:18:35 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2017-01-31 07:50:28 +01:00
|
|
|
const cacheRev = fileInfo && fileInfo.get('rev') || null;
|
2015-12-08 20:18:35 +01:00
|
|
|
if (cacheRev && storage.stat) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Stat file');
|
2016-07-17 13:30:38 +02:00
|
|
|
storage.stat(params.path, params.opts, (err, stat) => {
|
2016-06-05 13:48:23 +02:00
|
|
|
if (fileInfo && storage.name !== 'file' && (err || stat && stat.rev === cacheRev)) {
|
2015-12-13 22:18:02 +01:00
|
|
|
logger.info('Open file from cache because ' + (err ? 'stat error' : 'it is latest'), err);
|
2016-09-02 18:39:58 +02:00
|
|
|
this.openFileFromCache(params, callback, fileInfo);
|
2015-12-08 20:18:35 +01:00
|
|
|
} else if (stat) {
|
2016-03-13 09:34:36 +01:00
|
|
|
logger.info('Open file from storage (' + stat.rev + ', local ' + cacheRev + ')');
|
2015-12-08 20:18:35 +01:00
|
|
|
storageLoad();
|
2015-12-07 20:07:56 +01:00
|
|
|
} else {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Stat error', err);
|
2015-12-07 20:07:56 +01:00
|
|
|
callback(err);
|
|
|
|
}
|
2015-12-08 20:18:35 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
storageLoad();
|
|
|
|
}
|
2016-03-19 10:27:26 +01:00
|
|
|
} else {
|
2016-06-05 13:48:23 +02:00
|
|
|
logger.info('Open file from cache, will sync after load', params.storage);
|
2016-07-17 13:30:38 +02:00
|
|
|
this.openFileFromCache(params, (err, file) => {
|
2016-03-19 10:27:26 +01:00
|
|
|
if (!err && file) {
|
|
|
|
logger.info('Sync just opened file');
|
2016-09-02 18:39:58 +02:00
|
|
|
_.defer(() => this.syncFile(file));
|
2016-03-19 10:27:26 +01:00
|
|
|
}
|
|
|
|
callback(err);
|
|
|
|
}, fileInfo);
|
2015-12-07 20:07:56 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
openFileFromCache: function(params, callback, fileInfo) {
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage.cache.load(fileInfo.id, null, (err, data) => {
|
2015-12-12 16:43:43 +01:00
|
|
|
new Logger('open', params.name).info('Loaded file from cache', err);
|
2015-12-07 20:07:56 +01:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
} else {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.openFileWithData(params, callback, fileInfo, data);
|
2015-12-07 20:07:56 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
openFileWithData: function(params, callback, fileInfo, data, updateCacheOnSuccess) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('open', params.name);
|
2017-06-01 21:35:56 +02:00
|
|
|
let needLoadKeyFile = false;
|
2016-12-11 14:39:59 +01:00
|
|
|
if (!params.keyFileData && fileInfo && fileInfo.get('keyFileName')) {
|
2016-02-14 12:20:21 +01:00
|
|
|
params.keyFileName = fileInfo.get('keyFileName');
|
2016-12-11 14:39:59 +01:00
|
|
|
if (this.settings.get('rememberKeyFiles') === 'data') {
|
|
|
|
params.keyFileData = FileModel.createKeyFileWithHash(fileInfo.get('keyFileHash'));
|
|
|
|
} else if (this.settings.get('rememberKeyFiles') === 'path' && fileInfo.get('keyFilePath')) {
|
|
|
|
params.keyFilePath = fileInfo.get('keyFilePath');
|
|
|
|
if (Storage.file.enabled) {
|
2017-06-01 21:35:56 +02:00
|
|
|
needLoadKeyFile = true;
|
2016-12-11 14:39:59 +01:00
|
|
|
}
|
|
|
|
}
|
2017-12-07 18:46:46 +01:00
|
|
|
} else if (params.keyFilePath && !params.keyFileData && !fileInfo) {
|
2017-12-03 20:31:54 +01:00
|
|
|
needLoadKeyFile = true;
|
2016-02-14 12:20:21 +01:00
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const file = new FileModel({
|
2016-06-05 12:25:50 +02:00
|
|
|
id: fileInfo ? fileInfo.id : IdGenerator.uuid(),
|
2015-12-06 21:32:41 +01:00
|
|
|
name: params.name,
|
|
|
|
storage: params.storage,
|
|
|
|
path: params.path,
|
2016-08-16 22:11:54 +02:00
|
|
|
keyFileName: params.keyFileName,
|
2016-12-11 14:39:59 +01:00
|
|
|
keyFilePath: params.keyFilePath,
|
2017-04-14 23:22:33 +02:00
|
|
|
backup: fileInfo && fileInfo.get('backup') || null,
|
|
|
|
fingerprint: fileInfo && fileInfo.get('fingerprint') || null
|
2015-12-06 21:32:41 +01:00
|
|
|
});
|
2017-06-01 21:35:56 +02:00
|
|
|
const openComplete = err => {
|
2015-12-07 20:07:56 +01:00
|
|
|
if (err) {
|
2015-12-06 21:32:41 +01:00
|
|
|
return callback(err);
|
|
|
|
}
|
2016-07-17 13:30:38 +02:00
|
|
|
if (this.files.get(file.id)) {
|
2015-12-07 20:07:56 +01:00
|
|
|
return callback('Duplicate file id');
|
|
|
|
}
|
2015-12-10 22:31:47 +01:00
|
|
|
if (fileInfo && fileInfo.get('modified')) {
|
|
|
|
if (fileInfo.get('editState')) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Loaded local edit state');
|
2015-12-10 22:31:47 +01:00
|
|
|
file.setLocalEditState(fileInfo.get('editState'));
|
|
|
|
}
|
2016-03-19 10:27:26 +01:00
|
|
|
logger.info('Mark file as modified');
|
2015-12-11 18:50:44 +01:00
|
|
|
file.set('modified', true);
|
2015-12-11 21:51:16 +01:00
|
|
|
}
|
|
|
|
if (fileInfo) {
|
|
|
|
file.set('syncDate', fileInfo.get('syncDate'));
|
2015-12-10 22:31:47 +01:00
|
|
|
}
|
2016-02-06 11:59:57 +01:00
|
|
|
if (updateCacheOnSuccess) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Save loaded file to cache');
|
2016-06-05 12:25:50 +02:00
|
|
|
Storage.cache.save(file.id, null, params.fileData);
|
2015-12-07 20:07:56 +01:00
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const rev = params.rev || fileInfo && fileInfo.get('rev');
|
2016-07-17 13:30:38 +02:00
|
|
|
this.setFileOpts(file, params.opts);
|
|
|
|
this.addToLastOpenFiles(file, rev);
|
|
|
|
this.addFile(file);
|
2016-03-19 10:27:26 +01:00
|
|
|
callback(null, file);
|
2017-04-14 23:22:33 +02:00
|
|
|
this.fileOpened(file, data, params);
|
2017-06-01 21:35:56 +02:00
|
|
|
};
|
|
|
|
const open = () => {
|
|
|
|
file.open(params.password, data, params.keyFileData, openComplete);
|
|
|
|
};
|
|
|
|
if (needLoadKeyFile) {
|
|
|
|
Storage.file.load(params.keyFilePath, {}, (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
logger.info('Storage load error', err);
|
|
|
|
callback(err);
|
|
|
|
} else {
|
|
|
|
params.keyFileData = data;
|
|
|
|
open();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
open();
|
|
|
|
}
|
2015-12-06 21:32:41 +01:00
|
|
|
},
|
|
|
|
|
2016-03-04 18:06:18 +01:00
|
|
|
importFileWithXml: function(params, callback) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('import', params.name);
|
2016-03-04 18:06:18 +01:00
|
|
|
logger.info('File import request with supplied xml');
|
2017-01-31 07:50:28 +01:00
|
|
|
const file = new FileModel({
|
2016-06-05 12:25:50 +02:00
|
|
|
id: IdGenerator.uuid(),
|
2016-03-01 04:29:20 +01:00
|
|
|
name: params.name,
|
|
|
|
storage: params.storage,
|
|
|
|
path: params.path
|
|
|
|
});
|
2016-07-17 13:30:38 +02:00
|
|
|
file.importWithXml(params.fileXml, err => {
|
2016-03-04 18:06:18 +01:00
|
|
|
logger.info('Import xml complete ' + (err ? 'with error' : ''), err);
|
2016-03-01 04:29:20 +01:00
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2016-07-17 13:30:38 +02:00
|
|
|
this.addFile(file);
|
|
|
|
this.fileOpened(file);
|
2015-12-06 21:32:41 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2015-12-13 15:59:41 +01:00
|
|
|
addToLastOpenFiles: function(file, rev) {
|
2016-06-05 12:25:50 +02:00
|
|
|
this.appLogger.debug('Add last open file', file.id, file.get('name'), file.get('storage'), file.get('path'), rev);
|
2017-01-31 07:50:28 +01:00
|
|
|
const dt = new Date();
|
|
|
|
const fileInfo = new FileInfoModel({
|
2016-06-05 12:25:50 +02:00
|
|
|
id: file.id,
|
2015-12-06 21:32:41 +01:00
|
|
|
name: file.get('name'),
|
|
|
|
storage: file.get('storage'),
|
|
|
|
path: file.get('path'),
|
2016-03-19 13:43:50 +01:00
|
|
|
opts: this.getStoreOpts(file),
|
2015-12-06 21:32:41 +01:00
|
|
|
modified: file.get('modified'),
|
2015-12-10 22:31:47 +01:00
|
|
|
editState: file.getLocalEditState(),
|
2015-12-07 22:00:44 +01:00
|
|
|
rev: rev,
|
2015-12-11 21:51:16 +01:00
|
|
|
syncDate: file.get('syncDate') || dt,
|
2016-08-20 10:51:26 +02:00
|
|
|
openDate: dt,
|
2017-02-05 22:01:46 +01:00
|
|
|
backup: file.get('backup'),
|
2017-04-14 23:22:33 +02:00
|
|
|
fingerprint: file.get('fingerprint')
|
2015-12-06 21:32:41 +01:00
|
|
|
});
|
2016-12-11 14:39:59 +01:00
|
|
|
switch (this.settings.get('rememberKeyFiles')) {
|
|
|
|
case 'data':
|
|
|
|
fileInfo.set({
|
|
|
|
keyFileName: file.get('keyFileName') || null,
|
|
|
|
keyFileHash: file.getKeyFileHash()
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'path':
|
|
|
|
fileInfo.set({
|
|
|
|
keyFileName: file.get('keyFileName') || null,
|
|
|
|
keyFilePath: file.get('keyFilePath') || null
|
|
|
|
});
|
2016-02-14 12:20:21 +01:00
|
|
|
}
|
2016-06-05 12:25:50 +02:00
|
|
|
this.fileInfos.remove(file.id);
|
2015-12-06 23:13:29 +01:00
|
|
|
this.fileInfos.unshift(fileInfo);
|
2015-12-06 21:32:41 +01:00
|
|
|
this.fileInfos.save();
|
|
|
|
},
|
|
|
|
|
2016-03-19 13:43:50 +01:00
|
|
|
getStoreOpts: function(file) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const opts = file.get('opts');
|
|
|
|
const storage = file.get('storage');
|
2016-07-17 13:30:38 +02:00
|
|
|
if (Storage[storage] && Storage[storage].fileOptsToStoreOpts && opts) {
|
2016-03-19 13:43:50 +01:00
|
|
|
return Storage[storage].fileOptsToStoreOpts(opts, file);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
|
|
|
setFileOpts: function(file, opts) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const storage = file.get('storage');
|
2016-07-17 13:30:38 +02:00
|
|
|
if (Storage[storage] && Storage[storage].storeOptsToFileOpts && opts) {
|
2016-03-22 18:22:21 +01:00
|
|
|
file.set('opts', Storage[storage].storeOptsToFileOpts(opts, file));
|
2016-03-19 13:43:50 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-04-14 23:22:33 +02:00
|
|
|
fileOpened: function(file, data, params) {
|
2016-02-06 11:59:57 +01:00
|
|
|
if (file.get('storage') === 'file') {
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage.file.watch(file.get('path'), _.debounce(() => {
|
|
|
|
this.syncFile(file);
|
2016-02-06 11:59:57 +01:00
|
|
|
}, Timeouts.FileChangeSync));
|
|
|
|
}
|
2016-07-03 18:46:43 +02:00
|
|
|
if (file.isKeyChangePending(true)) {
|
|
|
|
Backbone.trigger('key-change-pending', { file: file });
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const backup = file.get('backup');
|
2016-08-21 18:46:44 +02:00
|
|
|
if (data && backup && backup.enabled && backup.pending) {
|
|
|
|
this.scheduleBackupFile(file, data);
|
|
|
|
}
|
2017-09-29 20:37:38 +02:00
|
|
|
if (params) {
|
|
|
|
this.saveFileFingerprint(file, params.password);
|
|
|
|
}
|
2016-02-06 11:59:57 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
fileClosed: function(file) {
|
|
|
|
if (file.get('storage') === 'file') {
|
|
|
|
Storage.file.unwatch(file.get('path'));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-12-06 21:32:41 +01:00
|
|
|
removeFileInfo: function(id) {
|
|
|
|
Storage.cache.remove(id);
|
|
|
|
this.fileInfos.remove(id);
|
|
|
|
this.fileInfos.save();
|
2015-12-08 22:00:31 +01:00
|
|
|
},
|
|
|
|
|
2016-02-14 12:20:21 +01:00
|
|
|
getFileInfo: function(file) {
|
2016-06-05 12:25:50 +02:00
|
|
|
return this.fileInfos.get(file.id) ||
|
2016-02-14 12:20:21 +01:00
|
|
|
this.fileInfos.getMatch(file.get('storage'), file.get('name'), file.get('path'));
|
|
|
|
},
|
|
|
|
|
2015-12-08 22:00:31 +01:00
|
|
|
syncFile: function(file, options, callback) {
|
2015-12-11 21:51:16 +01:00
|
|
|
if (file.get('demo')) {
|
|
|
|
return callback && callback();
|
|
|
|
}
|
2015-12-08 22:00:31 +01:00
|
|
|
if (file.get('syncing')) {
|
2015-12-11 18:50:44 +01:00
|
|
|
return callback && callback('Sync in progress');
|
|
|
|
}
|
|
|
|
if (!options) {
|
|
|
|
options = {};
|
2015-12-08 22:00:31 +01:00
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('sync', file.get('name'));
|
|
|
|
const storage = options.storage || file.get('storage');
|
|
|
|
let path = options.path || file.get('path');
|
|
|
|
const opts = options.opts || file.get('opts');
|
2016-04-07 07:11:59 +02:00
|
|
|
if (storage && Storage[storage].getPathForName && (!path || storage !== file.get('storage'))) {
|
2015-12-12 16:43:43 +01:00
|
|
|
path = Storage[storage].getPathForName(file.get('name'));
|
|
|
|
}
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Sync started', storage, path, options);
|
2017-01-31 07:50:28 +01:00
|
|
|
let fileInfo = this.getFileInfo(file);
|
2015-12-10 20:44:02 +01:00
|
|
|
if (!fileInfo) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Create new file info');
|
2017-01-31 07:50:28 +01:00
|
|
|
const dt = new Date();
|
2015-12-10 20:44:02 +01:00
|
|
|
fileInfo = new FileInfoModel({
|
|
|
|
id: IdGenerator.uuid(),
|
|
|
|
name: file.get('name'),
|
|
|
|
storage: file.get('storage'),
|
|
|
|
path: file.get('path'),
|
2016-03-19 13:43:50 +01:00
|
|
|
opts: this.getStoreOpts(file),
|
2015-12-10 20:44:02 +01:00
|
|
|
modified: file.get('modified'),
|
|
|
|
editState: null,
|
|
|
|
rev: null,
|
2015-12-11 21:51:16 +01:00
|
|
|
syncDate: dt,
|
2016-08-16 22:11:54 +02:00
|
|
|
openDate: dt,
|
|
|
|
backup: file.get('backup')
|
2015-12-10 20:44:02 +01:00
|
|
|
});
|
|
|
|
}
|
2015-12-11 21:51:16 +01:00
|
|
|
file.setSyncProgress();
|
2017-01-31 07:50:28 +01:00
|
|
|
const complete = (err, savedToCache) => {
|
2015-12-11 18:50:44 +01:00
|
|
|
if (!err) { savedToCache = true; }
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Sync finished', err || 'no error');
|
2015-12-11 18:50:44 +01:00
|
|
|
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
|
|
|
fileInfo.set({
|
2015-12-12 16:43:43 +01:00
|
|
|
name: file.get('name'),
|
2015-12-11 18:50:44 +01:00
|
|
|
storage: storage,
|
|
|
|
path: path,
|
2016-09-02 18:39:58 +02:00
|
|
|
opts: this.getStoreOpts(file),
|
2015-12-11 18:50:44 +01:00
|
|
|
modified: file.get('modified'),
|
2015-12-11 21:51:16 +01:00
|
|
|
editState: file.getLocalEditState(),
|
2016-06-05 12:25:50 +02:00
|
|
|
syncDate: file.get('syncDate')
|
2015-12-11 18:50:44 +01:00
|
|
|
});
|
2016-12-11 14:39:59 +01:00
|
|
|
if (this.settings.get('rememberKeyFiles') === 'data') {
|
2016-02-14 12:20:21 +01:00
|
|
|
fileInfo.set({
|
|
|
|
keyFileName: file.get('keyFileName') || null,
|
|
|
|
keyFileHash: file.getKeyFileHash()
|
|
|
|
});
|
|
|
|
}
|
2016-09-02 18:39:58 +02:00
|
|
|
if (!this.fileInfos.get(fileInfo.id)) {
|
|
|
|
this.fileInfos.unshift(fileInfo);
|
2015-12-11 18:50:44 +01:00
|
|
|
}
|
2016-09-02 18:39:58 +02:00
|
|
|
this.fileInfos.save();
|
2015-12-11 18:50:44 +01:00
|
|
|
if (callback) { callback(err); }
|
|
|
|
};
|
2015-12-10 20:44:02 +01:00
|
|
|
if (!storage) {
|
2016-06-05 12:25:50 +02:00
|
|
|
if (!file.get('modified') && fileInfo.id === file.id) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Local, not modified');
|
2015-12-10 22:31:47 +01:00
|
|
|
return complete();
|
2015-12-10 20:44:02 +01:00
|
|
|
}
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Local, save to cache');
|
2016-07-17 13:30:38 +02:00
|
|
|
file.getData((data, err) => {
|
2015-12-10 22:31:47 +01:00
|
|
|
if (err) { return complete(err); }
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Saved to cache', err || 'no error');
|
2015-12-10 22:31:47 +01:00
|
|
|
complete(err);
|
2016-08-21 18:46:44 +02:00
|
|
|
if (!err) {
|
|
|
|
this.scheduleBackupFile(file, data);
|
|
|
|
}
|
2015-12-10 20:44:02 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
2017-01-31 07:50:28 +01:00
|
|
|
const maxLoadLoops = 3;
|
|
|
|
let loadLoops = 0;
|
|
|
|
const loadFromStorageAndMerge = () => {
|
2015-12-10 20:44:02 +01:00
|
|
|
if (++loadLoops === maxLoadLoops) {
|
2015-12-12 16:43:43 +01:00
|
|
|
return complete('Too many load attempts');
|
2015-12-10 20:44:02 +01:00
|
|
|
}
|
2015-12-12 16:43:43 +01:00
|
|
|
logger.info('Load from storage, attempt ' + loadLoops);
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage[storage].load(path, opts, (err, data, stat) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Load from storage', stat, err || 'no error');
|
2015-12-10 22:31:47 +01:00
|
|
|
if (err) { return complete(err); }
|
2016-07-17 13:30:38 +02:00
|
|
|
file.mergeOrUpdate(data, options.remoteKey, (err) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Merge complete', err || 'no error');
|
2016-09-02 18:39:58 +02:00
|
|
|
this.refresh();
|
2016-01-16 21:32:50 +01:00
|
|
|
if (err) {
|
|
|
|
if (err.code === 'InvalidKey') {
|
|
|
|
logger.info('Remote key changed, request to enter new key');
|
|
|
|
Backbone.trigger('remote-key-changed', { file: file });
|
|
|
|
}
|
|
|
|
return complete(err);
|
|
|
|
}
|
2015-12-10 20:44:02 +01:00
|
|
|
if (stat && stat.rev) {
|
2015-12-12 16:43:43 +01:00
|
|
|
logger.info('Update rev in file info');
|
2015-12-10 20:44:02 +01:00
|
|
|
fileInfo.set('rev', stat.rev);
|
|
|
|
}
|
2015-12-11 21:51:16 +01:00
|
|
|
file.set('syncDate', new Date());
|
2015-12-10 20:44:02 +01:00
|
|
|
if (file.get('modified')) {
|
2016-06-05 13:48:23 +02:00
|
|
|
logger.info('Updated sync date, saving modified file');
|
2015-12-10 22:31:47 +01:00
|
|
|
saveToCacheAndStorage();
|
2016-02-06 11:59:57 +01:00
|
|
|
} else if (file.get('dirty')) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Saving not modified dirty file to cache');
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
2015-12-11 21:51:16 +01:00
|
|
|
if (err) { return complete(err); }
|
|
|
|
file.set('dirty', false);
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Complete, remove dirty flag');
|
2015-12-11 21:51:16 +01:00
|
|
|
complete();
|
|
|
|
});
|
2015-12-10 20:44:02 +01:00
|
|
|
} else {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Complete, no changes');
|
2015-12-10 22:31:47 +01:00
|
|
|
complete();
|
2015-12-10 20:44:02 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
2017-01-31 07:50:28 +01:00
|
|
|
const saveToStorage = (data) => {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Save data to storage');
|
2017-05-07 19:05:29 +02:00
|
|
|
const storageRev = fileInfo.get('storage') === storage ? fileInfo.get('rev') : undefined;
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage[storage].save(path, opts, data, (err, stat) => {
|
2015-12-10 22:31:47 +01:00
|
|
|
if (err && err.revConflict) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Save rev conflict, reloading from storage');
|
2015-12-10 22:31:47 +01:00
|
|
|
loadFromStorageAndMerge();
|
|
|
|
} else if (err) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Error saving data to storage');
|
2015-12-10 22:31:47 +01:00
|
|
|
complete(err);
|
|
|
|
} else {
|
2015-12-12 16:43:43 +01:00
|
|
|
if (stat && stat.rev) {
|
|
|
|
logger.info('Update rev in file info');
|
|
|
|
fileInfo.set('rev', stat.rev);
|
|
|
|
}
|
2016-03-27 20:14:31 +02:00
|
|
|
if (stat && stat.path) {
|
|
|
|
logger.info('Update path in file info', stat.path);
|
|
|
|
file.set('path', stat.path);
|
|
|
|
fileInfo.set('path', stat.path);
|
|
|
|
path = stat.path;
|
|
|
|
}
|
2015-12-11 21:51:16 +01:00
|
|
|
file.set('syncDate', new Date());
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Save to storage complete, update sync date');
|
2016-08-21 18:46:44 +02:00
|
|
|
this.scheduleBackupFile(file, data);
|
2015-12-10 22:31:47 +01:00
|
|
|
complete();
|
|
|
|
}
|
2017-05-07 19:05:29 +02:00
|
|
|
}, storageRev);
|
2015-12-10 22:31:47 +01:00
|
|
|
};
|
2017-05-02 21:22:08 +02:00
|
|
|
const saveToCacheAndStorage = () => {
|
|
|
|
logger.info('Getting file data for saving');
|
|
|
|
file.getData((data, err) => {
|
|
|
|
if (err) { return complete(err); }
|
|
|
|
if (storage === 'file') {
|
|
|
|
logger.info('Saving to file storage');
|
|
|
|
saveToStorage(data);
|
|
|
|
} else if (!file.get('dirty')) {
|
|
|
|
logger.info('Saving to storage, skip cache because not dirty');
|
|
|
|
saveToStorage(data);
|
|
|
|
} else {
|
|
|
|
logger.info('Saving to cache');
|
|
|
|
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
|
|
|
if (err) { return complete(err); }
|
|
|
|
file.set('dirty', false);
|
|
|
|
logger.info('Saved to cache, saving to storage');
|
|
|
|
saveToStorage(data);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Stat file');
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage[storage].stat(path, opts, (err, stat) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
if (err) {
|
|
|
|
if (err.notFound) {
|
|
|
|
logger.info('File does not exist in storage, creating');
|
|
|
|
saveToCacheAndStorage();
|
|
|
|
} else if (file.get('dirty')) {
|
|
|
|
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
2016-07-17 13:30:38 +02:00
|
|
|
file.getData((data) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
if (data) {
|
2016-07-17 13:30:38 +02:00
|
|
|
Storage.cache.save(fileInfo.id, null, data, (e) => {
|
2016-02-06 11:59:57 +01:00
|
|
|
if (!e) {
|
|
|
|
file.set('dirty', false);
|
|
|
|
}
|
|
|
|
logger.info('Saved to cache, exit with error', err || 'no error');
|
|
|
|
complete(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2015-12-10 20:44:02 +01:00
|
|
|
} else {
|
2016-02-06 11:59:57 +01:00
|
|
|
logger.info('Stat error, not dirty', err || 'no error');
|
|
|
|
complete(err);
|
2015-12-10 20:44:02 +01:00
|
|
|
}
|
2016-02-06 11:59:57 +01:00
|
|
|
} else if (stat.rev === fileInfo.get('rev')) {
|
|
|
|
if (file.get('modified')) {
|
2016-06-05 13:48:23 +02:00
|
|
|
logger.info('Stat found same version, modified, saving');
|
2016-02-06 11:59:57 +01:00
|
|
|
saveToCacheAndStorage();
|
|
|
|
} else {
|
|
|
|
logger.info('Stat found same version, not modified');
|
|
|
|
complete();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.info('Found new version, loading from storage');
|
|
|
|
loadFromStorageAndMerge();
|
|
|
|
}
|
|
|
|
});
|
2015-12-10 20:44:02 +01:00
|
|
|
}
|
2016-02-14 12:20:21 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
clearStoredKeyFiles: function() {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.fileInfos.each(fileInfo => {
|
2016-02-14 12:20:21 +01:00
|
|
|
fileInfo.set({
|
|
|
|
keyFileName: null,
|
2016-12-11 14:39:59 +01:00
|
|
|
keyFilePath: null,
|
2016-02-14 12:20:21 +01:00
|
|
|
keyFileHash: null
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.fileInfos.save();
|
2016-08-16 23:24:08 +02:00
|
|
|
},
|
|
|
|
|
2018-12-28 13:16:19 +01:00
|
|
|
unsetKeyFile: function (fileId) {
|
2018-12-28 18:51:03 +01:00
|
|
|
const fileInfo = this.fileInfos.get(fileId);
|
2018-12-28 13:16:19 +01:00
|
|
|
fileInfo.set({
|
|
|
|
keyFileName: null,
|
|
|
|
keyFilePath: null,
|
|
|
|
keyFileHash: null
|
|
|
|
});
|
2018-12-28 18:51:03 +01:00
|
|
|
this.fileInfos.save();
|
2018-12-28 13:16:19 +01:00
|
|
|
},
|
|
|
|
|
2016-08-16 23:24:08 +02:00
|
|
|
setFileBackup: function(fileId, backup) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const fileInfo = this.fileInfos.get(fileId);
|
2016-08-16 23:24:08 +02:00
|
|
|
if (fileInfo) {
|
|
|
|
fileInfo.set('backup', backup);
|
|
|
|
}
|
|
|
|
this.fileInfos.save();
|
2016-08-20 10:01:33 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
backupFile: function(file, data, callback) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const opts = file.get('opts');
|
2016-08-20 10:01:33 +02:00
|
|
|
let backup = file.get('backup');
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('backup', file.get('name'));
|
2016-08-20 10:01:33 +02:00
|
|
|
if (!backup || !backup.storage || !backup.path) {
|
|
|
|
return callback('Invalid backup settings');
|
|
|
|
}
|
|
|
|
let path = backup.path.replace('{date}', Format.dtStrFs(new Date()));
|
|
|
|
logger.info('Backup file to', backup.storage, path);
|
2017-01-31 07:50:28 +01:00
|
|
|
const saveToFolder = () => {
|
2016-08-21 17:43:59 +02:00
|
|
|
if (Storage[backup.storage].getPathForName) {
|
|
|
|
path = Storage[backup.storage].getPathForName(path);
|
|
|
|
}
|
|
|
|
Storage[backup.storage].save(path, opts, data, (err) => {
|
|
|
|
if (err) {
|
|
|
|
logger.error('Backup error', err);
|
|
|
|
} else {
|
|
|
|
logger.info('Backup complete');
|
2016-08-21 18:46:44 +02:00
|
|
|
backup = file.get('backup');
|
|
|
|
backup.lastTime = Date.now();
|
|
|
|
delete backup.pending;
|
|
|
|
file.set('backup', backup);
|
|
|
|
this.setFileBackup(file.id, backup);
|
2016-08-21 17:43:59 +02:00
|
|
|
}
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
let folderPath = UrlUtil.fileToDir(path);
|
|
|
|
if (Storage[backup.storage].getPathForName) {
|
|
|
|
folderPath = Storage[backup.storage].getPathForName(folderPath).replace('.kdbx', '');
|
|
|
|
}
|
2017-02-05 23:34:37 +01:00
|
|
|
Storage[backup.storage].stat(folderPath, opts, err => {
|
2016-08-20 10:01:33 +02:00
|
|
|
if (err) {
|
2016-08-21 17:43:59 +02:00
|
|
|
if (err.notFound) {
|
|
|
|
logger.info('Backup folder does not exist');
|
|
|
|
if (!Storage[backup.storage].mkdir) {
|
|
|
|
return callback('Mkdir not supported by ' + backup.storage);
|
|
|
|
}
|
2017-02-05 23:34:37 +01:00
|
|
|
Storage[backup.storage].mkdir(folderPath, err => {
|
2016-08-21 17:43:59 +02:00
|
|
|
if (err) {
|
|
|
|
logger.error('Error creating backup folder', err);
|
|
|
|
callback('Error creating backup folder');
|
|
|
|
} else {
|
|
|
|
logger.info('Backup folder created');
|
|
|
|
saveToFolder();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
logger.error('Stat folder error', err);
|
|
|
|
callback('Cannot stat backup folder');
|
|
|
|
}
|
2016-08-20 10:01:33 +02:00
|
|
|
} else {
|
2016-08-21 17:43:59 +02:00
|
|
|
logger.info('Backup folder exists, saving');
|
|
|
|
saveToFolder();
|
2016-08-20 10:01:33 +02:00
|
|
|
}
|
|
|
|
});
|
2016-08-21 18:46:44 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
scheduleBackupFile: function(file, data) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const backup = file.get('backup');
|
2016-08-21 18:46:44 +02:00
|
|
|
if (!backup || !backup.enabled) {
|
|
|
|
return;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const logger = new Logger('backup', file.get('name'));
|
2016-08-21 18:46:44 +02:00
|
|
|
let needBackup = false;
|
|
|
|
if (!backup.lastTime) {
|
|
|
|
needBackup = true;
|
|
|
|
logger.debug('No last backup time, backup now');
|
|
|
|
} else {
|
2017-01-31 07:50:28 +01:00
|
|
|
const dt = new Date(backup.lastTime);
|
2016-08-21 18:46:44 +02:00
|
|
|
switch (backup.schedule) {
|
|
|
|
case '0':
|
|
|
|
break;
|
|
|
|
case '1d':
|
|
|
|
dt.setDate(dt.getDate() + 1);
|
|
|
|
break;
|
|
|
|
case '1w':
|
|
|
|
dt.setDate(dt.getDate() + 7);
|
|
|
|
break;
|
|
|
|
case '1m':
|
|
|
|
dt.setMonth(dt.getMonth() + 1);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (dt.getTime() <= Date.now()) {
|
|
|
|
needBackup = true;
|
|
|
|
}
|
|
|
|
logger.debug('Last backup time: ' + new Date(backup.lastTime) +
|
|
|
|
', schedule: ' + backup.schedule +
|
|
|
|
', next time: ' + dt +
|
|
|
|
', ' + (needBackup ? 'backup now' : 'skip backup'));
|
|
|
|
}
|
|
|
|
if (!backup.pending) {
|
|
|
|
backup.pending = true;
|
|
|
|
this.setFileBackup(file.id, backup);
|
|
|
|
}
|
|
|
|
if (needBackup) {
|
|
|
|
this.backupFile(file, data, _.noop);
|
|
|
|
}
|
2017-04-14 23:22:33 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
saveFileFingerprint: function(file, password) {
|
|
|
|
if (Launcher && Launcher.fingerprints && password) {
|
|
|
|
const fileInfo = this.fileInfos.get(file.id);
|
|
|
|
Launcher.fingerprints.register(file.id, this.params.password, token => {
|
|
|
|
if (token) {
|
|
|
|
fileInfo.set('fingerprint', token);
|
|
|
|
this.model.fileInfos.save();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = AppModel;
|