UNPKG

connect-stream

Version:

A module for streaming static files. Does etags, caching, partial response, etc.

416 lines (392 loc) 13.9 kB
(function() { var Stream; module.exports = function(root, opts) { var stream; stream = new Stream(root, opts); return function(req, res, next) { res.stream = function(src, opt, cb) { if (opt == null) { opt = {}; } if (cb == null) { cb = function() {}; } if (typeof opt === 'function') { cb = opt; } return stream.serve(src, opt, cb, req, res, next); }; return next(); }; }; Stream = (function() { var Negotiator, asyncCache, fs, http, mime, path, url, zlib; fs = require('fs'); url = require('url'); path = require('path'); http = require('http'); zlib = require('zlib'); mime = require('mime'); Negotiator = require('negotiator'); asyncCache = require('async-cache'); Stream.prototype.opts = { root: process.cwd(), trim: true, concatenate: 'join', passthrough: false, cache: { fd: { max: 1000, maxAge: 1000 * 60 * 60 }, stat: { max: 5000, maxAge: 1000 * 60 }, content: { max: 1024 * 1024 * 64, maxAge: 1000 * 60 * 10 } } }; Stream.prototype.fdman = (require('fd'))(); Stream.prototype.store = { fd: null, stat: null, content: null }; function Stream(root, opts) { var _ref, _ref1, _ref10, _ref11, _ref12, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9; if (root == null) { root = '/'; } if (opts == null) { opts = {}; } if (root) { this.opts.root = root; } if ((opts.concatenate != null) && ((_ref = opts.concatenate) === 'join' || _ref === 'resolve')) { this.opts.concatenate = opts.concatenate; } if (opts.passthrough != null) { this.opts.passthrough = opts.passthrough; } if (opts.debug != null) { this.opts.debug = opts.debug; } if (((_ref1 = opts.cache) != null ? (_ref2 = _ref1.fd) != null ? _ref2.max : void 0 : void 0) != null) { this.opts.cache.fd.max = opts.cache.fd.max; } if (((_ref3 = opts.cache) != null ? (_ref4 = _ref3.fd) != null ? _ref4.maxAge : void 0 : void 0) != null) { this.opts.cache.fd.maxAge = opts.cache.fd.maxAge; } if (((_ref5 = opts.cache) != null ? (_ref6 = _ref5.stat) != null ? _ref6.max : void 0 : void 0) != null) { this.opts.cache.stat.max = opts.cache.stat.max; } if (((_ref7 = opts.cache) != null ? (_ref8 = _ref7.stat) != null ? _ref8.maxAge : void 0 : void 0) != null) { this.opts.cache.stat.maxAge = opts.cache.stat.maxAge; } if (((_ref9 = opts.cache) != null ? (_ref10 = _ref9.content) != null ? _ref10.max : void 0 : void 0) != null) { this.opts.cache.content.max = opts.cache.content.max; } if (((_ref11 = opts.cache) != null ? (_ref12 = _ref11.content) != null ? _ref12.maxAge : void 0 : void 0) != null) { this.opts.cache.content.maxAge = opts.cache.content.maxAge; } if (opts.cache === false) { this.opts.cache.fd.max = 1; this.opts.cache.fd.maxAge = 0; this.opts.cache.fd.length = function() { return Infinity; }; this.opts.cache.stat.max = 1; this.opts.cache.stat.maxAge = 0; this.opts.cache.stat.length = function() { return Infinity; }; this.opts.cache.content.max = 1; this.opts.cache.content.maxAge = 0; this.opts.cache.content.length = function() { return Infinity; }; } else { this.opts.cache.fd.length = function(n) { return n.length; }; this.opts.cache.stat.length = function(n) { return n.length; }; this.opts.cache.content.length = function(n) { return n.length; }; } this.store.fd = asyncCache({ max: this.opts.cache.fd.max, maxAge: this.opts.cache.fd.maxAge, length: this.opts.cache.fd.length, load: this.fdman.open.bind(this.fdman), dispose: this.fdman.close.bind(this.fdman) }); this.store.stat = asyncCache({ max: this.opts.cache.stat.max, maxAge: this.opts.cache.stat.maxAge, length: this.opts.cache.stat.length, load: (function(_this) { return function(key, cb) { var fd, fdp, p, _ref13; if (!(fdp = key.match(/^(\d+):(.*)/))) { return fs.stat(key, cb); } _ref13 = [+fdp[1], fdp[2]], fd = _ref13[0], p = _ref13[1]; return fs.fstat(fd, function(err, stat) { if (err) { return cb(err); } _this.store.stat.set(p, stat); return cb(null, stat); }); }; })(this) }); this.store.content = asyncCache({ max: this.opts.cache.content.max, maxAge: this.opts.cache.content.maxAge, length: this.opts.cache.content.length, load: function() { throw new Error('This should not ever happen'); } }); } Stream.prototype.parseRange = function(stat, req) { var end, ini, range, ranges, _i, _len, _ref, _ref1, _ref2, _ref3; if (((_ref = req.headers) != null ? _ref.range : void 0) != null) { ranges = []; _ref1 = req.headers.range.replace('bytes=', '').split(','); for (_i = 0, _len = _ref1.length; _i < _len; _i++) { range = _ref1[_i]; _ref2 = range.split('-'), ini = _ref2[0], end = _ref2[1]; if (ini.length === 0) { ini = stat.size - end; if (ini < 0) { ini = 0; } end = stat.size - 1; } if (end.length === 0) { end = stat.size - 1; } _ref3 = [+ini, +end], ini = _ref3[0], end = _ref3[1]; ranges.push({ ini: ini, end: end }); } return ranges; } return null; }; Stream.prototype.isAcceptGzip = function(src, req) { var gz, neg; gz = false; if (!/\.t?gz$/.exec(src)) { neg = req.negotiator || new Negotiator(req); gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip'; } return gz; }; Stream.prototype.isValidRange = function(ini, end) { if (ini > end) { return false; } return true; }; Stream.prototype.error = function(err, res, next, fdend) { if (fdend) { fdend(); } if (typeof err === 'number') { res.statusCode = err; } else { res.statusCode = (function() { switch (err.code) { case 'ENOENT': case 'EISDIR': return 404; case 'EPERM': case 'EACCES': return 403; default: return 500; } })(); } if (this.opts.passthrough && res.statusCode === 404) { return next(); } return next(err); }; Stream.prototype.cache = function(res, fdend) { fdend(); res.statusCode = 304; return res.end(); }; Stream.prototype.serve = function(src, opt, cb, req, res, next) { if (!src) { throw new Error('`src` should not be blank, res.stream(src, callback).'); } if (typeof cb !== 'function') { console.error('`callback` should be function, res.stream(src, callback).'); cb = function() {}; } src = path[this.opts.concatenate](this.opts.root, src); if (this.opts.trim) { src = decodeURIComponent(url.parse(src).pathname); } if (!src) { return next(); } return this.store.fd.get(src, (function(_this) { return function(err, fd) { var fdend; if (err) { cb(err, null); return _this.error(err, res, next); } _this.fdman.checkout(src, fd); fdend = _this.fdman.checkinfn(src, fd); return _this.store.stat.get("" + fd + ":" + src, function(err, stat) { var buf, cache, ctype, end, etag, gzbuf, gzstream, ini, isFirstStream, match, partial, range, ranges, since, storekey, stream, _ref, _ref1, _ref2, _ref3; if (err) { cb(err, null); return _this.error(err, res, next, fdend); } ranges = _this.parseRange(stat, req); if (ranges === null) { partial = false; _ref = [0, stat.size - 1], ini = _ref[0], end = _ref[1]; isFirstStream = true; } else { if (ranges.length !== 1) { console.error('not supported multi range-spec'); } range = ranges[0]; partial = true; _ref1 = [range.ini, range.end], ini = _ref1[0], end = _ref1[1]; isFirstStream = ini === 0 && (end === 0 || end === 1); } if (!_this.isValidRange(ini, end)) { res.statusCode = 416; res.setHeader('content-length', 0); cb(new Error('out of range'), [ini, end], isFirstStream); return res.end(); } if ((since = req.headers['if-modified-since'])) { since = (new Date(since)).getTime(); if (since && since >= stat.mtime.getTime()) { cb(null, [ini, end], isFirstStream); return _this.cache(res, fdend); } } etag = "\"" + stat.dev + "-" + stat.ino + "-" + (stat.mtime.getTime()) + "\""; if ((match = req.headers['if-none-match'])) { if (match === etag) { cb(null, [ini, end], isFirstStream); return _this.cache(res, fdend); } } if ((match = req.headers['if-range'])) { if (match === etag) { cb(null, [ini, end], isFirstStream); return _this.cache(res, fdend); } } if (stat.isDirectory()) { err = new Error; err.code = 'EISDIR'; cb(err, [ini, end], isFirstStream); return _this.error(err, res, next, fdend); } cache = ((_ref2 = opt.headers) != null ? _ref2['cache-control'] : void 0) || 'public'; ctype = ((_ref3 = opt.headers) != null ? _ref3['content-type'] : void 0) || mime.lookup(path.extname(src)); res.setHeader('cache-control', cache); res.setHeader('last-modified', stat.mtime.toUTCString()); res.setHeader('etag', etag); res.setHeader('content-type', ctype); if (!partial) { res.statusCode = 200; } else { res.statusCode = 206; if (stat.size < end - ini + 1) { end = stat.size - 1; } res.setHeader('content-range', "bytes " + ini + "-" + end + "/" + stat.size); } storekey = "" + fd + ":" + stat.size + ":" + etag; if (!partial && _this.store.content.has(storekey)) { return _this.store.content.get(storekey, function(err, content) { fdend(); if (err) { cb(err, [ini, end], isFirstStream); return _this.error(err, res, next); } if (_this.isAcceptGzip(src, req) && content.gz) { res.setHeader('content-encoding', 'gzip'); res.setHeader('content-length', content.gz.length); cb(null, [ini, end], isFirstStream); return res.end(content.gz); } else { res.setHeader('content-length', content.length); cb(null, [ini, end], isFirstStream); return res.end(content); } }); } else { stream = fs.createReadStream(src, { fd: fd, start: ini, end: end }); stream.destroy = function() {}; stream.on('error', function(err) { err = err.stack || err.message; console.error('Error serving %s fd=%d\n%s', src, fd, err); res.socket.destroy(); return fdend(); }); gzstream = zlib.createGzip(); if (!partial && _this.isAcceptGzip(src, req)) { res.setHeader('content-encoding', 'gzip'); stream.pipe(gzstream); gzstream.pipe(res); if (_this.store.content._cache.max > stat.size) { buf = []; gzbuf = []; stream.on('data', function(chunk) { return buf.push(chunk); }); gzstream.on('data', function(chunk) { return gzbuf.push(chunk); }); gzstream.on('end', function() { var content; content = Buffer.concat(buf); content.gz = Buffer.concat(gzbuf); return _this.store.content.set(storekey, content); }); } } else { res.setHeader('content-length', end - ini + 1); stream.pipe(res); } return stream.on('end', function() { cb(null, [ini, end], isFirstStream); return process.nextTick(fdend); }); } }); }; })(this)); }; return Stream; })(); }).call(this);