mirror of https://github.com/keeweb/keeweb.git
track changes in local files
This commit is contained in:
parent
31307d7710
commit
08c61056b5
|
@ -50,6 +50,16 @@ if (window.process && window.process.versions && window.process.versions.electro
|
|||
deleteFile: function(path) {
|
||||
this.req('fs').unlinkSync(path);
|
||||
},
|
||||
statFile: function(path) {
|
||||
return this.req('fs').statSync(path);
|
||||
},
|
||||
parsePath: function(fileName) {
|
||||
var path = this.req('path');
|
||||
return { path: fileName, dir: path.dirname(fileName), file: path.basename(fileName) };
|
||||
},
|
||||
createFsWatcher: function(path) {
|
||||
return this.req('fs').watch(path, { persistent: false });
|
||||
},
|
||||
preventExit: function(e) {
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
var Timeouts = {
|
||||
AutoSync: 30 * 1000 * 60,
|
||||
CopyTip: 1500,
|
||||
AutoHideHint: 3000
|
||||
AutoHideHint: 3000,
|
||||
FileChangeSync: 3000
|
||||
};
|
||||
|
||||
module.exports = Timeouts;
|
||||
|
|
|
@ -11,6 +11,7 @@ var Backbone = require('backbone'),
|
|||
FileModel = require('./file-model'),
|
||||
FileInfoModel = require('./file-info-model'),
|
||||
Storage = require('../storage'),
|
||||
Timeouts = require('../const/timeouts'),
|
||||
IdGenerator = require('../util/id-generator'),
|
||||
Logger = require('../util/logger');
|
||||
|
||||
|
@ -108,7 +109,11 @@ var AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
closeAllFiles: function() {
|
||||
this.files.each(function(file) { file.close(); });
|
||||
var that = this;
|
||||
this.files.each(function(file) {
|
||||
file.close();
|
||||
that.fileClosed(file);
|
||||
});
|
||||
this.files.reset();
|
||||
this.menu.groupsSection.removeAllItems();
|
||||
this.menu.tagsSection.set('scrollable', false);
|
||||
|
@ -119,6 +124,8 @@ var AppModel = Backbone.Model.extend({
|
|||
},
|
||||
|
||||
closeFile: function(file) {
|
||||
file.close();
|
||||
this.fileClosed(file);
|
||||
this.files.remove(file);
|
||||
this.updateTags();
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
|
@ -352,13 +359,14 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
var cacheId = fileInfo && fileInfo.id || IdGenerator.uuid();
|
||||
file.set('cacheId', cacheId);
|
||||
if (updateCacheOnSuccess && params.storage !== 'file') {
|
||||
if (updateCacheOnSuccess) {
|
||||
logger.info('Save loaded file to cache');
|
||||
Storage.cache.save(cacheId, params.fileData);
|
||||
}
|
||||
var rev = params.rev || fileInfo && fileInfo.get('rev');
|
||||
that.addToLastOpenFiles(file, rev);
|
||||
that.addFile(file);
|
||||
that.fileOpened(file);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -381,6 +389,21 @@ var AppModel = Backbone.Model.extend({
|
|||
this.fileInfos.save();
|
||||
},
|
||||
|
||||
fileOpened: function(file) {
|
||||
var that = this;
|
||||
if (file.get('storage') === 'file') {
|
||||
Storage.file.watch(file.get('path'), _.debounce(function() {
|
||||
that.syncFile(file);
|
||||
}, Timeouts.FileChangeSync));
|
||||
}
|
||||
},
|
||||
|
||||
fileClosed: function(file) {
|
||||
if (file.get('storage') === 'file') {
|
||||
Storage.file.unwatch(file.get('path'));
|
||||
}
|
||||
},
|
||||
|
||||
removeFileInfo: function(id) {
|
||||
Storage.cache.remove(id);
|
||||
this.fileInfos.remove(id);
|
||||
|
@ -425,7 +448,7 @@ var AppModel = Backbone.Model.extend({
|
|||
file.setSyncProgress();
|
||||
var complete = function(err, savedToCache) {
|
||||
if (!err) { savedToCache = true; }
|
||||
logger.info('Sync finished', err);
|
||||
logger.info('Sync finished', err || 'no error');
|
||||
file.setSyncComplete(path, storage, err ? err.toString() : null, savedToCache);
|
||||
file.set('cacheId', fileInfo.id);
|
||||
fileInfo.set({
|
||||
|
@ -452,7 +475,7 @@ var AppModel = Backbone.Model.extend({
|
|||
file.getData(function(data, err) {
|
||||
if (err) { return complete(err); }
|
||||
Storage.cache.save(fileInfo.id, data, function(err) {
|
||||
logger.info('Saved to cache', err);
|
||||
logger.info('Saved to cache', err || 'no error');
|
||||
complete(err);
|
||||
});
|
||||
});
|
||||
|
@ -464,10 +487,10 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
logger.info('Load from storage, attempt ' + loadLoops);
|
||||
Storage[storage].load(path, function(err, data, stat) {
|
||||
logger.info('Load from storage', stat, err);
|
||||
logger.info('Load from storage', stat, err || 'no error');
|
||||
if (err) { return complete(err); }
|
||||
file.mergeOrUpdate(data, options.remoteKey, function(err) {
|
||||
logger.info('Merge complete', err);
|
||||
logger.info('Merge complete', err || 'no error');
|
||||
that.refresh();
|
||||
if (err) {
|
||||
if (err.code === 'InvalidKey') {
|
||||
|
@ -484,7 +507,7 @@ var AppModel = Backbone.Model.extend({
|
|||
if (file.get('modified')) {
|
||||
logger.info('Updated sync date, saving modified file to cache and storage');
|
||||
saveToCacheAndStorage();
|
||||
} else if (file.get('dirty') && storage !== 'file') {
|
||||
} else if (file.get('dirty')) {
|
||||
logger.info('Saving not modified dirty file to cache');
|
||||
Storage.cache.save(fileInfo.id, data, function (err) {
|
||||
if (err) { return complete(err); }
|
||||
|
@ -503,8 +526,8 @@ var AppModel = Backbone.Model.extend({
|
|||
logger.info('Save to cache and storage');
|
||||
file.getData(function(data, err) {
|
||||
if (err) { return complete(err); }
|
||||
if (!file.get('dirty') || storage === 'file') {
|
||||
logger.info('Save to storage, skip cache because not dirty or file storage');
|
||||
if (!file.get('dirty')) {
|
||||
logger.info('Save to storage, skip cache because not dirty');
|
||||
saveToStorage(data);
|
||||
} else {
|
||||
logger.info('Saving to cache');
|
||||
|
@ -527,9 +550,6 @@ var AppModel = Backbone.Model.extend({
|
|||
logger.info('Error saving data to storage');
|
||||
complete(err);
|
||||
} else {
|
||||
if (storage === 'file') {
|
||||
Storage.cache.remove(fileInfo.id);
|
||||
}
|
||||
if (stat && stat.rev) {
|
||||
logger.info('Update rev in file info');
|
||||
fileInfo.set('rev', stat.rev);
|
||||
|
@ -540,55 +560,42 @@ var AppModel = Backbone.Model.extend({
|
|||
}
|
||||
}, fileInfo.get('rev'));
|
||||
};
|
||||
if (options.reload) {
|
||||
logger.info('Saved to cache');
|
||||
loadFromStorageAndMerge();
|
||||
} else if (storage === 'file') {
|
||||
if (file.get('modified') || file.get('path') !== path) {
|
||||
logger.info('Save modified file to storage');
|
||||
saveToCacheAndStorage();
|
||||
} else {
|
||||
logger.info('Skip not modified file');
|
||||
complete();
|
||||
}
|
||||
} else {
|
||||
logger.info('Stat file');
|
||||
Storage[storage].stat(path, function (err, stat) {
|
||||
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);
|
||||
file.getData(function (data) {
|
||||
if (data) {
|
||||
Storage.cache.save(fileInfo.id, data, function (e) {
|
||||
if (!e) {
|
||||
file.set('dirty', false);
|
||||
}
|
||||
logger.info('Saved to cache, exit with error', err);
|
||||
complete(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info('Stat error, not dirty', err);
|
||||
complete(err);
|
||||
}
|
||||
} else if (stat.rev === fileInfo.get('rev')) {
|
||||
if (file.get('modified')) {
|
||||
logger.info('Stat found same version, modified, saving to cache and storage');
|
||||
saveToCacheAndStorage();
|
||||
} else {
|
||||
logger.info('Stat found same version, not modified');
|
||||
complete();
|
||||
}
|
||||
logger.info('Stat file');
|
||||
Storage[storage].stat(path, function (err, stat) {
|
||||
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');
|
||||
file.getData(function (data) {
|
||||
if (data) {
|
||||
Storage.cache.save(fileInfo.id, data, function (e) {
|
||||
if (!e) {
|
||||
file.set('dirty', false);
|
||||
}
|
||||
logger.info('Saved to cache, exit with error', err || 'no error');
|
||||
complete(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info('Found new version, loading from storage');
|
||||
loadFromStorageAndMerge();
|
||||
logger.info('Stat error, not dirty', err || 'no error');
|
||||
complete(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (stat.rev === fileInfo.get('rev')) {
|
||||
if (file.get('modified')) {
|
||||
logger.info('Stat found same version, modified, saving to cache and storage');
|
||||
saveToCacheAndStorage();
|
||||
} else {
|
||||
logger.info('Stat found same version, not modified');
|
||||
complete();
|
||||
}
|
||||
} else {
|
||||
logger.info('Found new version, loading from storage');
|
||||
loadFromStorageAndMerge();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,6 +5,8 @@ var Launcher = require('../comp/launcher'),
|
|||
|
||||
var logger = new Logger('storage-file');
|
||||
|
||||
var fileWatchers = {};
|
||||
|
||||
var StorageFile = {
|
||||
name: 'file',
|
||||
enabled: !!Launcher,
|
||||
|
@ -14,25 +16,95 @@ var StorageFile = {
|
|||
var ts = logger.ts();
|
||||
try {
|
||||
var data = Launcher.readFile(path);
|
||||
logger.debug('Loaded', path, logger.ts(ts));
|
||||
if (callback) { callback(null, data.buffer); }
|
||||
var rev = Launcher.statFile(path).mtime.getTime().toString();
|
||||
logger.debug('Loaded', path, rev, logger.ts(ts));
|
||||
if (callback) { callback(null, data.buffer, { rev: rev }); }
|
||||
} catch (e) {
|
||||
logger.error('Error reading local file', path, e);
|
||||
if (callback) { callback(e, null); }
|
||||
}
|
||||
},
|
||||
|
||||
save: function(path, data, callback) {
|
||||
logger.debug('Save', path);
|
||||
stat: function(path, callback) {
|
||||
logger.debug('Stat', path);
|
||||
var ts = logger.ts();
|
||||
try {
|
||||
var stat = Launcher.statFile(path);
|
||||
logger.debug('Stat done', path, logger.ts(ts));
|
||||
if (callback) { callback(null, { rev: stat.mtime.getTime().toString() }); }
|
||||
} catch (e) {
|
||||
logger.error('Error stat local file', path, e);
|
||||
if (e.code === 'ENOENT') {
|
||||
e.notFound = true;
|
||||
}
|
||||
if (callback) { callback(e, null); }
|
||||
}
|
||||
},
|
||||
|
||||
save: function(path, data, callback, rev) {
|
||||
logger.debug('Save', path, rev);
|
||||
var ts = logger.ts();
|
||||
try {
|
||||
if (rev) {
|
||||
try {
|
||||
var stat = Launcher.statFile(path);
|
||||
var fileRev = stat.mtime.getTime().toString();
|
||||
if (fileRev !== rev) {
|
||||
logger.debug('Save mtime differs', rev, fileRev);
|
||||
if (callback) { callback({ revConflict: true }, { rev: fileRev }); }
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// file doesn't exist or we cannot stat it: don't care and overwrite
|
||||
}
|
||||
}
|
||||
Launcher.writeFile(path, data);
|
||||
var newRev = Launcher.statFile(path).mtime.getTime().toString();
|
||||
logger.debug('Saved', path, logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
if (callback) { callback(undefined, { rev: newRev }); }
|
||||
} catch (e) {
|
||||
logger.error('Error writing local file', path, e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
},
|
||||
|
||||
watch: function(path, callback) {
|
||||
var names = Launcher.parsePath(path);
|
||||
if (!fileWatchers[names.dir]) {
|
||||
logger.debug('Watch dir', names.dir);
|
||||
var fsWatcher = Launcher.createFsWatcher(names.dir);
|
||||
fsWatcher.on('change', this.fsWatcherChange.bind(this, names.dir));
|
||||
fileWatchers[names.dir] = { fsWatcher: fsWatcher, callbacks: [] };
|
||||
}
|
||||
fileWatchers[names.dir].callbacks.push({ file: names.file, callback: callback });
|
||||
},
|
||||
|
||||
unwatch: function(path) {
|
||||
var names = Launcher.parsePath(path);
|
||||
var watcher = fileWatchers[names.dir];
|
||||
if (watcher) {
|
||||
var ix = watcher.callbacks.findIndex(function(cb) { return cb.file === names.file; });
|
||||
if (ix >= 0) {
|
||||
watcher.callbacks.splice(ix, 1);
|
||||
}
|
||||
if (!watcher.callbacks.length) {
|
||||
logger.debug('Stop watch dir', names.dir);
|
||||
watcher.fsWatcher.close();
|
||||
delete fileWatchers[names.dir];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fsWatcherChange: function(dirname, evt, fileName) {
|
||||
var watcher = fileWatchers[dirname];
|
||||
if (watcher) {
|
||||
watcher.callbacks.forEach(function(cb) {
|
||||
if (cb.file === fileName && typeof cb.callback === 'function') {
|
||||
logger.debug('File changed', dirname, evt, fileName);
|
||||
cb.callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ Release notes
|
|||
-------------
|
||||
##### v1.0.0 (not released yet)
|
||||
Performance, stability and quality improvements
|
||||
`+` track changes in local files
|
||||
`+` mobile layout made more convenient
|
||||
`-` #80: prevent data loss on group move
|
||||
`-` issues with clipboard clear fixed
|
||||
|
|
Loading…
Reference in New Issue