diff --git a/lib/gzip.js b/lib/gzip.js index 7594bab..71487b4 100644 --- a/lib/gzip.js +++ b/lib/gzip.js @@ -4,8 +4,6 @@ * MIT Licensed */ -var spawn = require('child_process').spawn; - /** * Connect middleware providing gzip compression on the fly. By default, it * compresses requests with mime types that match the expression @@ -14,28 +12,40 @@ var spawn = require('child_process').spawn; * Options: * * - `matchType` Regular expression matching mime types to be compressed - * - `flags` String of flags passed to the binary. Nothing by default - * - `bin` Binary executable defaulting to "gzip" + * - gzip options chunkSize, windowBits, level, memLevel, strategy, flush * * @param {Object} options * @api public */ +var zlib = require('zlib'); + +function hasOwnProperty(o, key) { + return Object.prototype.hasOwnProperty.call(o, key); +} +function selectProperties(o, key1, key2) { + var object = {}; + for (var i = 1, ii = arguments.length; i < ii; i++) { + var key = arguments[i]; + if (hasOwnProperty(o, key)) { + object[key] = o[key]; + } + } + return object; +} + exports = module.exports = function gzip(options) { var options = options || {}, matchType = options.matchType || /text|javascript|json/, - bin = options.bin || 'gzip', - flags = options.flags || ''; + gzipOptions = selectProperties(options, + 'chunkSize', 'windowBits', 'level', 'memLevel', 'strategy', 'flush'); if (!matchType.test) throw new Error('option matchType must be a regular expression'); - - flags = (flags) ? '-c ' + flags : '-c'; - flags = flags.split(' '); - + return function gzip(req, res, next) { var writeHead = res.writeHead, defaults = {}; - + ['write', 'end'].forEach(function(name) { defaults[name] = res[name]; res[name] = function() { @@ -46,7 +56,7 @@ exports = module.exports = function gzip(options) { res[name].apply(this, arguments); }; }); - + res.writeHead = function(code) { var args = Array.prototype.slice.call(arguments, 0), write = defaults.write, @@ -58,12 +68,12 @@ exports = module.exports = function gzip(options) { res.setHeader(key, headers[key]); } } - + ua = req.headers['user-agent'] || ''; accept = req.headers['accept-encoding'] || ''; type = res.getHeader('content-type') || ''; encoding = res.getHeader('content-encoding'); - + if (req.method === 'HEAD' || code !== 200 || !~accept.indexOf('gzip') || !matchType.test(type) || encoding || (~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1'))) { @@ -71,40 +81,75 @@ exports = module.exports = function gzip(options) { res.end = end; return finish(); } - + res.setHeader('Content-Encoding', 'gzip'); res.setHeader('Vary', 'Accept-Encoding'); + var hasLength = res.get('Content-Length'); res.removeHeader('Content-Length'); - - gzip = spawn(bin, flags); - + + var gzip = zlib.createGzip(gzipOptions); + res.write = function(chunk, encoding) { - gzip.stdin.write(chunk, encoding); + gzip.write(chunk, encoding); }; res.end = function(chunk, encoding) { if (chunk) { res.write(chunk, encoding); } - gzip.stdin.end(); + gzip.end(); }; - gzip.stdout.addListener('data', function(chunk) { - write.call(res, chunk); - }); + if(hasLength) { + // if length is defined, send the compressed content as whole + var chunks = []; + gzip.addListener('data', function(chunk) { + chunks.push(chunk); + }); - gzip.addListener('exit', function(code) { - res.write = write; - res.end = end; - res.end(); - }); - - finish(); - - function finish() { - res.writeHead = writeHead; - res.writeHead.apply(res, args); + gzip.addListener('end', function() { + res.write = write; + res.end = end; + + var l = 0; + for(var i=0; i < chunks.length; i++) { + l += chunks[i].length; + } + res.setHeader('Content-Length', l); + finish(); + + for(var i=0; i < chunks.length; i++) { + res.write( chunks[i] ); + } + res.end(); + }); + + gzip.addListener('error', function(error) { + finish(); + res.close(); + }); + + } else { + gzip.addListener('data', function(chunk) { + write.call(res, chunk); + }); + + gzip.addListener('end', function() { + res.write = write; + res.end = end; + res.end(); + }); + + gzip.addListener('error', function(error) { + res.close(); + }); + + finish(); } + function finish() { + res.writeHead = writeHead; + res.writeHead.apply(res, args); + } }; next(); diff --git a/lib/staticGzip.js b/lib/staticGzip.js index 43573c0..d52b505 100644 --- a/lib/staticGzip.js +++ b/lib/staticGzip.js @@ -8,8 +8,8 @@ var fs = require('fs'), parse = require('url').parse, path = require('path'), mime = require('mime'), - exec = require('child_process').exec, - staticSend = require('connect').static.send; + zlib = require('zlib'), + send = require('send'); /** * staticGzip gzips statics and then serves them with the regular Connect @@ -19,8 +19,8 @@ var fs = require('fs'), * Options: * * - `matchType` Regular expression matching mime types to be compressed - * - `flags` String of flags passed to the binary. Defaults to "--best" - * - `bin` Binary executable defaulting to "gzip" + * - `flags` DEPRECATED: String of flags passed to the binary. Nothing + * by default * * @param {String} root * @param {Object} options @@ -30,30 +30,28 @@ var fs = require('fs'), exports = module.exports = function staticGzip(root, options) { var options = options || {}, matchType = options.matchType || /text|javascript|json/, - bin = options.bin || 'gzip', - flags = options.flags || '--best', rootLength; if (!root) throw new Error('staticGzip root must be set'); if (!matchType.test) throw new Error('option matchType must be a regular expression'); - + options.root = root; rootLength = root.length; - + return function(req, res, next) { var url, filename, type, acceptEncoding, ua; - - if (req.method !== 'GET') return next(); - + + if (req.method !== 'GET' && req.method !== 'HEAD' ) return next(); + url = parse(req.url); filename = path.join(root, url.pathname); if ('/' == filename[filename.length - 1]) filename += 'index.html'; - + type = mime.lookup(filename); if (!matchType.test(type)) { return passToStatic(filename); } - + acceptEncoding = req.headers['accept-encoding'] || ''; if (!~acceptEncoding.indexOf('gzip')) { return passToStatic(filename); @@ -63,38 +61,55 @@ exports = module.exports = function staticGzip(root, options) { if (~ua.indexOf('MSIE 6') && !~ua.indexOf('SV1')) { return passToStatic(filename); } - + // Potentially malicious path if (~filename.indexOf('..')) { return passToStatic(filename); } - + // Check for requested file fs.stat(filename, function(err, stat) { if (err || stat.isDirectory()) { - return passToStatic(filename); + return next(); } - + // Check for compressed file var base = path.basename(filename), dir = path.dirname(filename), gzipname = path.join(dir, base + '.' + Number(stat.mtime) + '.gz'); fs.stat(gzipname, function(err) { if (err && err.code === 'ENOENT') { - // Remove any old gz files - exec('rm ' + path.join(dir, base + '.*.gz'), function(err) { - // Gzipped file doesn't exist, so make it then send - gzip(bin, flags, filename, gzipname, function(err) { - return sendGzip(); - }); - }); + + var gzip = zlib.createGzip() + // Gzipped file doesn't exist, so make it and then send + + // First write compressed data to a temporary file. This + // avoids race condition when several node instances are + // competing for the same file and other node instance + // would try to send half-done file. + var tmpname = gzipname + '.' + process.pid + '.tmp'; + var outfile = fs.createWriteStream( tmpname ); + var infile = fs.createReadStream( filename ); + + outfile.on('close', function() { + // compressed data has been written to the temporary + // file and file descriptor is closed. + fs.rename( tmpname, gzipname, function() { + // temporary file renamed to final file + return sendGzip(); + }); + }); + + // pipe compressed data to file + infile.pipe(gzip).pipe(outfile); + } else if (err) { - return passToStatic(filename); + return passToStatic(filename); } else { - return sendGzip(); + return sendGzip(); } }); - + function sendGzip() { var charset = mime.charsets.lookup(type), contentType = type + (charset ? '; charset=' + charset : ''); @@ -104,22 +119,15 @@ exports = module.exports = function staticGzip(root, options) { passToStatic(gzipname); } }); - + + // send file function passToStatic(name) { - var o = Object.create(options); - o.path = name.substr(rootLength); - staticSend(req, res, next, o); + send(req, name.substr(rootLength)) + .root(options.root) + .maxage(options.maxAge || 0) + // in case of error just pass through + .on('error', function() { next(); }) + .pipe(res); } }; }; - -function gzip(bin, flags, src, dest, callback) { - var cmd = bin + ' ' + flags + ' -c ' + src + ' > ' + dest; - exec(cmd, function(err, stdout, stderr) { - if (err) { - console.error('\n' + err.stack); - fs.unlink(dest); - } - callback(err); - }); -} diff --git a/package.json b/package.json index a460efe..6476251 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "connect-gzip", "description": "Gzip middleware for Connect. Based on implementation in Connect 0.5.9. Original source: https://github.com/senchalabs/connect/tree/c9a0c1e0e98451bb5fffb70c622b827a11bf4fc7", - "version": "0.1.5", + "version": "0.1.5-f6", "author": "Nate Smith", "main": "./index.js", "dependencies": { - "connect": ">=1 <2", + "send": ">=0.1", "mime": ">=0.0.1" }, "engines": { - "node": "*" + "node": ">=0.6.0" }, "devDependencies": { "expresso": ">=0.9", "should": ">=0.3.1" } -} \ No newline at end of file +}