track changes in local files

This commit is contained in:
Antelle 2016-02-06 13:59:57 +03:00
parent 31307d7710
commit 08c61056b5
5 changed files with 156 additions and 65 deletions

View File

@ -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;

View File

@ -3,7 +3,8 @@
var Timeouts = {
AutoSync: 30 * 1000 * 60,
CopyTip: 1500,
AutoHideHint: 3000
AutoHideHint: 3000,
FileChangeSync: 3000
};
module.exports = Timeouts;

View File

@ -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();
}
});
}
}
});

View File

@ -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();
}
});
}
}
};

View File

@ -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