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.
383 lines
9.5 KiB
383 lines
9.5 KiB
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;
|
|
|