file-fuser
Version:
subserver to combine files for caching
225 lines (200 loc) • 7.62 kB
JavaScript
var path = require("path"),
util = require("util"),
fs = require("fs"),
async = require("async"),
url = require("url"),
crypto = require("crypto"),
fWatcher = require("watch-interface"),
eventStream = require("event-stream"),
sourcemap = require("source-map");
// helper
var debug = false;
function log(/*args*/) {
if (debug) console.log.apply(console, arguments);
}
// FileFuser
function FileFuser(options) {
if (!options) throw new Error('FileFuser requires config object');
this.baseDirectory = options.baseDirectory;
this.files = options.files;
this.combinedFile = options.combinedFile;
this.fileWatcher = null;
this.combinedFileBuildTime = null;
this.sourceMapFile = options.combinedFile + '.jsm';
this.sourceRoot = options.sourceRoot;
}
FileFuser.prototype.getSourceMapFilePath = function() {
return path.join(this.baseDirectory, this.sourceMapFile);
}
FileFuser.prototype.getCombinedFilePath = function() {
return path.join(this.baseDirectory, this.combinedFile);
}
FileFuser.prototype.getSourceMapFileStream = function(thenDo) {
var stream;
try {
stream = fs.createReadStream(this.getSourceMapFilePath());
} catch (e) { thenDo(e, stream); }
thenDo(null, stream);
}
FileFuser.prototype.getCombinedFileStream = function(thenDo) {
var stream;
try {
stream = fs.createReadStream(this.getCombinedFilePath());
} catch (e) { thenDo(e, stream); }
thenDo(null, stream);
}
function createHeader(timestamp, files) {
return util.format('// This file was generated on %s\n\n'
+ 'JSLoader.expectToLoadModules([%s]);\n\n',
timestamp.toGMTString(),
files.map(function(fn) { return "'" + fn + "'" }));
}
FileFuser.prototype.writeFilesInto = function(baseDirectory, files, thenDo) {
var time = this.combinedFileBuildTime = new Date(),
targetFilePath = this.getCombinedFilePath(),
targetFileStream = fs.createWriteStream(targetFilePath),
jsmFilePath = this.getSourceMapFilePath(),
jsmFileStream = fs.createWriteStream(jsmFilePath),
jsmGenerator = new sourcemap.SourceMapGenerator({
file: this.sourceMapFile,
sourceRoot: this.sourceRoot
}),
headerTask = function(next) { targetFileStream.write(createHeader(time, files)); next(); },
lineNo = 6,
linesInFile,
writeFileTasks = [headerTask].concat(files.map(function(file) {
var fullPath = path.join(baseDirectory, file);
return function(next) {
targetFileStream.write(';// ' + file + ':\n');
linesInFile = 1;
var reader = fs.createReadStream(fullPath);
reader.on('error', function(err) { next(err); });
eventStream.pipeline(
reader,
eventStream.through(function write(data) {
linesInFile += data.toString('utf8').split(/\r\n|[\n\r\u0085\u2028\u2029]/g).length-1;
targetFileStream.write(data);
}, function end() {
for (var i = 0; i <= linesInFile; i++) {
jsmGenerator.addMapping({
generated: { line: lineNo + i, column: 1 },
original: { line: i + 1, column: 1 },
source: file
});
}
targetFileStream.write('\n\n');
lineNo += linesInFile + 2;
next();
})
);
}
}));
log('Creating combined file %s from [%s]', targetFilePath, files);
async.series(writeFileTasks, function(err) {
if (err) { console.log('error writing %s: %s', targetFilePath, err); }
targetFileStream.end();
jsmFileStream.end(jsmGenerator.toString());
thenDo(err);
});
}
FileFuser.prototype.checkIfCombinedFilesAreUpToDate = function(watcher, thenDo) {
var targetFilePath = this.getCombinedFilePath(),
lastBuildTime = this.combinedFileBuildTime;
async.waterfall([
function(next) {
fs.exists(targetFilePath, function(exists) {
next(null, exists); });
},
function(combinedFileExists, next) {
if (!combinedFileExists) thenDo(null, false);
else watcher.getChangesSince(lastBuildTime, next);
},
function(changes, next) { thenDo(null, changes.length === 0); }
], thenDo);
}
FileFuser.prototype.computeHash = function(combinedFileStream, thenDo) {
combinedFileStream.on('error', function(err) { thenDo(err); });
var md5sum = crypto.createHash('md5'), hash;
md5sum.setEncoding('hex');
md5sum.on('data', function(d) {hash = String(d); });
md5sum.on('error', function(err) { thenDo(err, null); });
md5sum.on('end', function() { thenDo(null, hash); });
combinedFileStream.pipe(md5sum);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// file watcher
FileFuser.prototype.ensureFileWatcher = function(thenDo) {
var self = this;
if (self.fileWatcher) { thenDo(null, self.fileWatcher); return; }
if (self._fileWatcherIsStarting) {
setTimeout(self.ensureFileWatcher.bind(self, thenDo), 200);
return;
}
self._fileWatcherIsStarting = true;
self._startSentinel = setTimeout(function() {
if (!self._fileWatcherIsStarting) return;
delete self._fileWatcherIsStarting;
delete self._startSentinel;
thenDo(new Error("file fuser timed out while starting on " + self.baseDirectory));
}, 20*1000);
fWatcher.onFiles(
self.baseDirectory, self.files,
{excludes: [/(.*\/|^).git(\/.*|$)/,/(^|.*\/)node_modules(\/.*|$)/,/R-libraries(\/.*|$)/]},
function(err, watcher) {
if (!self._fileWatcherIsStarting) return;
delete self._fileWatcherIsStarting;
if (err) { thenDo(err, null); return; }
self.fileWatcher = watcher;
thenDo(null, watcher);
});
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// main interface
FileFuser.prototype.withCombinedFileStreamDo = function(thenDo) {
var self = this;
async.waterfall([
function(next) { self.ensureFileWatcher(next); },
function(watcher, next) { self.checkIfCombinedFilesAreUpToDate(watcher, next); },
function(areUpToDate, next) {
if (areUpToDate) next();
else self.writeFilesInto(self.baseDirectory, self.files, next);
}
], function(err) {
if (err) { thenDo(err, null); return; }
self.getCombinedFileStream(thenDo);
});
}
FileFuser.prototype.withSourceMapStreamDo = function(thenDo) {
this.getSourceMapFileStream(thenDo);
}
FileFuser.prototype.withHashDo = function(doFunc) {
var self = this;
self.withCombinedFileStreamDo(function(err, combinedFileStream) {
if (err) { doFunc(err, null); return; }
self.computeHash(combinedFileStream, doFunc);
});
}
FileFuser.prototype.close = function(doFunc) {
var self = this;
if (!self.fileWatcher && !self._fileWatcherIsStarting) {
doFunc && doFunc(null); return; }
async.waterfall([
self.ensureFileWatcher.bind(self),
function(watcher, next) {
if (!watcher) next(); else watcher.close(next); }
], doFunc);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// exports
module.exports = function(options, thenDo) {
// options = {
// baseDirectory: STRING,
// files [STRING],
// combinedFile: STRING -- path to file that holds fused content
log("Acquiring fileFuser instance...");
var fileFuser = new FileFuser(options);
fileFuser.ensureFileWatcher(function(err, _) {
if (err) console.error("Error starting fileFuser isntance: " + err);
else log("fileFuser instance created");
thenDo(err, fileFuser); });
};