Telegram Web, preconfigured for usage in I2P. http://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

384 lines
9.5 KiB

11 years ago
var fs = require('fs');
var glob = require('glob');
var mm = require('minimatch');
var q = require('q');
var helper = require('./helper');
var log = require('./logger').create('watcher');
var createWinGlob = function(realGlob) {
return function(pattern, options, done) {
realGlob(pattern, options, function(err, results) {
done(err, results.map(helper.normalizeWinPath));
});
};
};
if (process.platform === 'win32') {
glob = createWinGlob(glob);
}
var File = function(path, mtime) {
// used for serving (processed path, eg some/file.coffee -> some/file.coffee.js)
this.path = path;
// original absolute path, id of the file
this.originalPath = path;
// where the content is stored (processed)
this.contentPath = path;
this.mtime = mtime;
this.isUrl = false;
};
var Url = function(path) {
this.path = path;
this.isUrl = true;
};
Url.prototype.toString = File.prototype.toString = function() {
return this.path;
};
var GLOB_OPTS = {
// globDebug: true,
cwd: '/'
};
var byPath = function(a, b) {
if (a.path > b.path) {
return 1;
}
if (a.path < b.path) {
return -1;
}
return 0;
};
// TODO(vojta): ignore changes (add/change/remove) when in the middle of refresh
// TODO(vojta): do not glob patterns that are watched (both on init and refresh)
var List = function(patterns, excludes, emitter, preprocess, batchInterval) {
var self = this;
var pendingDeferred;
var pendingTimeout;
var resolveFiles = function(buckets) {
var uniqueMap = {};
var files = {
served: [],
included: []
};
buckets.forEach(function(bucket, idx) {
bucket.sort(byPath).forEach(function(file) {
if (!uniqueMap[file.path]) {
if (patterns[idx].served) {
files.served.push(file);
}
if (patterns[idx].included) {
files.included.push(file);
}
uniqueMap[file.path] = true;
}
});
});
return files;
};
var resolveDeferred = function(files) {
if (pendingTimeout) {
clearTimeout(pendingTimeout);
}
pendingDeferred.resolve(files || resolveFiles(self.buckets));
pendingDeferred = pendingTimeout = null;
};
var fireEventAndDefer = function() {
if (pendingTimeout) {
clearTimeout(pendingTimeout);
}
if (!pendingDeferred) {
pendingDeferred = q.defer();
emitter.emit('file_list_modified', pendingDeferred.promise);
}
pendingTimeout = setTimeout(resolveDeferred, batchInterval);
};
// re-glob all the patterns
this.refresh = function() {
// TODO(vojta): cancel refresh if another refresh starts
var buckets = self.buckets = new Array(patterns.length);
var complete = function() {
if (buckets !== self.buckets) {
return;
}
var files = resolveFiles(buckets);
resolveDeferred(files);
log.debug('Resolved files:\n\t' + files.served.join('\n\t'));
};
// TODO(vojta): use some async helper library for this
var pending = 0;
var finish = function() {
pending--;
if (!pending) {
complete();
}
};
if (!pendingDeferred) {
pendingDeferred = q.defer();
emitter.emit('file_list_modified', pendingDeferred.promise);
}
if (pendingTimeout) {
clearTimeout(pendingTimeout);
}
patterns.forEach(function(patternObject, i) {
var pattern = patternObject.pattern;
if (helper.isUrlAbsolute(pattern)) {
buckets[i] = [new Url(pattern)];
return;
}
pending++;
glob(pattern, GLOB_OPTS, function(err, resolvedFiles) {
var matchedAndNotIgnored = 0;
buckets[i] = [];
if (!resolvedFiles.length) {
log.warn('Pattern "%s" does not match any file.', pattern);
return finish();
}
// stat each file to get mtime and isDirectory
resolvedFiles.forEach(function(path) {
var matchExclude = function(excludePattern) {
return mm(path, excludePattern);
};
if (excludes.some(matchExclude)) {
log.debug('Excluded file "%s"', path);
return;
}
pending++;
matchedAndNotIgnored++;
fs.stat(path, function(error, stat) {
if (error) {
log.debug('An error occured while reading "%s"', path);
finish();
} else {
if (!stat.isDirectory()) {
// TODO(vojta): reuse file objects
var file = new File(path, stat.mtime);
preprocess(file, function() {
buckets[i].push(file);
finish();
});
} else {
log.debug('Ignored directory "%s"', path);
finish();
}
}
});
});
if (!matchedAndNotIgnored) {
log.warn('All files matched by "%s" were excluded.', pattern);
}
finish();
});
});
if (!pending) {
process.nextTick(complete);
}
return pendingDeferred.promise;
};
// set new patterns and excludes
// and re-glob
this.reload = function(newPatterns, newExcludes) {
patterns = newPatterns;
excludes = newExcludes;
return this.refresh();
};
/**
* Adds a new file into the list (called by watcher)
* - ignore excluded files
* - ignore files that are already in the list
* - get mtime (by stat)
* - fires "file_list_modified"
*/
this.addFile = function(path, done) {
var buckets = this.buckets;
var i, j;
// sorry, this callback is just for easier testing
done = done || function() {};
// check excludes
for (i = 0; i < excludes.length; i++) {
if (mm(path, excludes[i])) {
log.debug('Add file "%s" ignored. Excluded by "%s".', path, excludes[i]);
return done();
}
}
for (i = 0; i < patterns.length; i++) {
if (mm(path, patterns[i].pattern)) {
for (j = 0; j < buckets[i].length; j++) {
if (buckets[i][j].originalPath === path) {
log.debug('Add file "%s" ignored. Already in the list.', path);
return done();
}
}
break;
}
}
if (i >= patterns.length) {
log.debug('Add file "%s" ignored. Does not match any pattern.', path);
return done();
}
var addedFile = new File(path);
buckets[i].push(addedFile);
return fs.stat(path, function(err, stat) {
// in the case someone refresh() the list before stat callback
if (self.buckets === buckets) {
addedFile.mtime = stat.mtime;
return preprocess(addedFile, function() {
// TODO(vojta): ignore if refresh/reload happens
log.info('Added file "%s".', path);
fireEventAndDefer();
done();
});
}
return done();
});
};
/**
* Update mtime of a file (called by watcher)
* - ignore if file is not in the list
* - ignore if mtime has not changed
* - fire "file_list_modified"
*/
this.changeFile = function(path, done) {
var buckets = this.buckets;
var i, j;
// sorry, this callback is just for easier testing
done = done || function() {};
outer:
for (i = 0; i < buckets.length; i++) {
for (j = 0; j < buckets[i].length; j++) {
if (buckets[i][j].originalPath === path) {
break outer;
}
}
}
if (!buckets[i]) {
log.debug('Changed file "%s" ignored. Does not match any file in the list.', path);
return done();
}
var changedFile = buckets[i][j];
return fs.stat(path, function(err, stat) {
// https://github.com/paulmillr/chokidar/issues/11
if (err || !stat) {
return self.removeFile(path, done);
}
if (self.buckets === buckets && stat.mtime > changedFile.mtime) {
log.info('Changed file "%s".', path);
changedFile.mtime = stat.mtime;
// TODO(vojta): THIS CAN MAKE FILES INCONSISTENT
// if batched change is resolved before preprocessing is finished, the file can be in
// inconsistent state, when the promise is resolved.
// Solutions:
// 1/ the preprocessor should not change the object in place, but create a copy that would
// be eventually merged into the original file, here in the callback, synchronously.
// 2/ delay the promise resolution - wait for any changeFile operations to finish
return preprocess(changedFile, function() {
// TODO(vojta): ignore if refresh/reload happens
fireEventAndDefer();
done();
});
}
return done();
});
};
/**
* Remove a file from the list (called by watcher)
* - ignore if file is not in the list
* - fire "file_list_modified"
*/
this.removeFile = function(path, done) {
var buckets = this.buckets;
// sorry, this callback is just for easier testing
done = done || function() {};
for (var i = 0; i < buckets.length; i++) {
for (var j = 0; j < buckets[i].length; j++) {
if (buckets[i][j].originalPath === path) {
buckets[i].splice(j, 1);
log.info('Removed file "%s".', path);
fireEventAndDefer();
return done();
}
}
}
log.debug('Removed file "%s" ignored. Does not match any file in the list.', path);
return done();
};
};
List.$inject = ['config.files', 'config.exclude', 'emitter', 'preprocess',
'config.autoWatchBatchDelay'];
// PUBLIC
exports.List = List;
exports.File = File;
exports.Url = Url;