diff --git a/HISTORY.md b/HISTORY.md index 31b77e5..a635d08 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +1.1.0 / 2024-09-10 +================== + + * Refactor `SendStream` to an ES6 class + 1.1.0 / 2024-09-10 ================== diff --git a/index.js b/index.js index dcd1d07..cd14fd5 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,6 @@ var parseRange = require('range-parser') var path = require('path') var statuses = require('statuses') var Stream = require('stream') -var util = require('util') /** * Path function references. @@ -91,680 +90,659 @@ function send (req, path, options) { * @private */ -function SendStream (req, path, options) { - Stream.call(this) +class SendStream extends Stream { + constructor (req, path, options) { + super() - var opts = options || {} + var opts = options || {} - this.options = opts - this.path = path - this.req = req + this.options = opts + this.path = path + this.req = req - this._acceptRanges = opts.acceptRanges !== undefined - ? Boolean(opts.acceptRanges) - : true + this._acceptRanges = opts.acceptRanges !== undefined + ? Boolean(opts.acceptRanges) + : true - this._cacheControl = opts.cacheControl !== undefined - ? Boolean(opts.cacheControl) - : true + this._cacheControl = opts.cacheControl !== undefined + ? Boolean(opts.cacheControl) + : true - this._etag = opts.etag !== undefined - ? Boolean(opts.etag) - : true + this._etag = opts.etag !== undefined + ? Boolean(opts.etag) + : true - this._dotfiles = opts.dotfiles !== undefined - ? opts.dotfiles - : 'ignore' + this._dotfiles = opts.dotfiles !== undefined + ? opts.dotfiles + : 'ignore' - if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') { - throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') - } - - this._extensions = opts.extensions !== undefined - ? normalizeList(opts.extensions, 'extensions option') - : [] - - this._immutable = opts.immutable !== undefined - ? Boolean(opts.immutable) - : false - - this._index = opts.index !== undefined - ? normalizeList(opts.index, 'index option') - : ['index.html'] - - this._lastModified = opts.lastModified !== undefined - ? Boolean(opts.lastModified) - : true - - this._maxage = opts.maxAge || opts.maxage - this._maxage = typeof this._maxage === 'string' - ? ms(this._maxage) - : Number(this._maxage) - this._maxage = !isNaN(this._maxage) - ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) - : 0 - - this._root = opts.root - ? resolve(opts.root) - : null -} - -/** - * Inherits from `Stream`. - */ - -util.inherits(SendStream, Stream) - -/** - * Emit error with `status`. - * - * @param {number} status - * @param {Error} [err] - * @private - */ - -SendStream.prototype.error = function error (status, err) { - // emit if listeners instead of responding - if (hasListeners(this, 'error')) { - return this.emit('error', createHttpError(status, err)) - } - - var res = this.res - var msg = statuses.message[status] || String(status) - var doc = createHtmlDocument('Error', escapeHtml(msg)) - - // clear existing headers - clearHeaders(res) - - // add error headers - if (err && err.headers) { - setHeaders(res, err.headers) - } + if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') { + throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') + } - // send basic response - res.statusCode = status - res.setHeader('Content-Type', 'text/html; charset=UTF-8') - res.setHeader('Content-Length', Buffer.byteLength(doc)) - res.setHeader('Content-Security-Policy', "default-src 'none'") - res.setHeader('X-Content-Type-Options', 'nosniff') - res.end(doc) -} + this._extensions = opts.extensions !== undefined + ? normalizeList(opts.extensions, 'extensions option') + : [] + + this._immutable = opts.immutable !== undefined + ? Boolean(opts.immutable) + : false + + this._index = opts.index !== undefined + ? normalizeList(opts.index, 'index option') + : ['index.html'] + + this._lastModified = opts.lastModified !== undefined + ? Boolean(opts.lastModified) + : true + + this._maxage = opts.maxAge || opts.maxage + this._maxage = typeof this._maxage === 'string' + ? ms(this._maxage) + : Number(this._maxage) + this._maxage = !isNaN(this._maxage) + ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) + : 0 + + this._root = opts.root + ? resolve(opts.root) + : null + } + + /** + * Emit error with `status`. + * + * @param {number} status + * @param {Error} [err] + * @private + */ + error (status, err) { + // emit if listeners instead of responding + if (hasListeners(this, 'error')) { + return this.emit('error', createHttpError(status, err)) + } -/** - * Check if the pathname ends with "/". - * - * @return {boolean} - * @private - */ + var res = this.res + var msg = statuses.message[status] || String(status) + var doc = createHtmlDocument('Error', escapeHtml(msg)) -SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () { - return this.path[this.path.length - 1] === '/' -} + // clear existing headers + clearHeaders(res) -/** - * Check if this is a conditional GET request. - * - * @return {Boolean} - * @api private - */ + // add error headers + if (err && err.headers) { + setHeaders(res, err.headers) + } -SendStream.prototype.isConditionalGET = function isConditionalGET () { - return this.req.headers['if-match'] || - this.req.headers['if-unmodified-since'] || - this.req.headers['if-none-match'] || - this.req.headers['if-modified-since'] -} + // send basic response + res.statusCode = status + res.setHeader('Content-Type', 'text/html; charset=UTF-8') + res.setHeader('Content-Length', Buffer.byteLength(doc)) + res.setHeader('Content-Security-Policy', "default-src 'none'") + res.setHeader('X-Content-Type-Options', 'nosniff') + res.end(doc) + } + + /** + * Check if the pathname ends with "/". + * + * @return {boolean} + * @private + */ + hasTrailingSlash () { + return this.path[this.path.length - 1] === '/' + } + + /** + * Check if this is a conditional GET request. + * + * @return {Boolean} + * @api private + */ + isConditionalGET () { + return this.req.headers['if-match'] || + this.req.headers['if-unmodified-since'] || + this.req.headers['if-none-match'] || + this.req.headers['if-modified-since'] + } + + /** + * Check if the request preconditions failed. + * + * @return {boolean} + * @private + */ + isPreconditionFailure () { + var req = this.req + var res = this.res + + // if-match + var match = req.headers['if-match'] + if (match) { + var etag = res.getHeader('ETag') + return !etag || (match !== '*' && parseTokenList(match).every(function (match) { + return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag + })) + } -/** - * Check if the request preconditions failed. - * - * @return {boolean} - * @private - */ + // if-unmodified-since + var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) + if (!isNaN(unmodifiedSince)) { + var lastModified = parseHttpDate(res.getHeader('Last-Modified')) + return isNaN(lastModified) || lastModified > unmodifiedSince + } -SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () { - var req = this.req - var res = this.res - - // if-match - var match = req.headers['if-match'] - if (match) { - var etag = res.getHeader('ETag') - return !etag || (match !== '*' && parseTokenList(match).every(function (match) { - return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag - })) + return false + } + + /** + * Strip various content header fields for a change in entity. + * + * @private + */ + removeContentHeaderFields () { + var res = this.res + + res.removeHeader('Content-Encoding') + res.removeHeader('Content-Language') + res.removeHeader('Content-Length') + res.removeHeader('Content-Range') + res.removeHeader('Content-Type') + } + + /** + * Respond with 304 not modified. + * + * @api private + */ + notModified () { + var res = this.res + debug('not modified') + this.removeContentHeaderFields() + res.statusCode = 304 + res.end() } - // if-unmodified-since - var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']) - if (!isNaN(unmodifiedSince)) { - var lastModified = parseHttpDate(res.getHeader('Last-Modified')) - return isNaN(lastModified) || lastModified > unmodifiedSince + /** + * Raise error that headers already sent. + * + * @api private + */ + headersAlreadySent () { + var err = new Error('Can\'t set headers after they are sent.') + debug('headers already sent') + this.error(500, err) + } + + /** + * Check if the request is cacheable, aka + * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). + * + * @return {Boolean} + * @api private + */ + isCachable () { + var statusCode = this.res.statusCode + return (statusCode >= 200 && statusCode < 300) || + statusCode === 304 + } + + /** + * Handle stat() error. + * + * @param {Error} error + * @private + */ + onStatError (error) { + switch (error.code) { + case 'ENAMETOOLONG': + case 'ENOENT': + case 'ENOTDIR': + this.error(404, error) + break + default: + this.error(500, error) + break + } } - return false -} - -/** - * Strip various content header fields for a change in entity. - * - * @private - */ - -SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () { - var res = this.res - - res.removeHeader('Content-Encoding') - res.removeHeader('Content-Language') - res.removeHeader('Content-Length') - res.removeHeader('Content-Range') - res.removeHeader('Content-Type') -} - -/** - * Respond with 304 not modified. - * - * @api private - */ - -SendStream.prototype.notModified = function notModified () { - var res = this.res - debug('not modified') - this.removeContentHeaderFields() - res.statusCode = 304 - res.end() -} - -/** - * Raise error that headers already sent. - * - * @api private - */ - -SendStream.prototype.headersAlreadySent = function headersAlreadySent () { - var err = new Error('Can\'t set headers after they are sent.') - debug('headers already sent') - this.error(500, err) -} - -/** - * Check if the request is cacheable, aka - * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). - * - * @return {Boolean} - * @api private - */ - -SendStream.prototype.isCachable = function isCachable () { - var statusCode = this.res.statusCode - return (statusCode >= 200 && statusCode < 300) || - statusCode === 304 -} - -/** - * Handle stat() error. - * - * @param {Error} error - * @private - */ - -SendStream.prototype.onStatError = function onStatError (error) { - switch (error.code) { - case 'ENAMETOOLONG': - case 'ENOENT': - case 'ENOTDIR': - this.error(404, error) - break - default: - this.error(500, error) - break + /** + * Check if the cache is fresh. + * + * @return {Boolean} + * @api private + */ + isFresh () { + return fresh(this.req.headers, { + etag: this.res.getHeader('ETag'), + 'last-modified': this.res.getHeader('Last-Modified') + }) } -} -/** - * Check if the cache is fresh. - * - * @return {Boolean} - * @api private - */ - -SendStream.prototype.isFresh = function isFresh () { - return fresh(this.req.headers, { - etag: this.res.getHeader('ETag'), - 'last-modified': this.res.getHeader('Last-Modified') - }) -} - -/** - * Check if the range is fresh. - * - * @return {Boolean} - * @api private - */ + /** + * Check if the range is fresh. + * + * @return {Boolean} + * @api private + */ + isRangeFresh () { + var ifRange = this.req.headers['if-range'] -SendStream.prototype.isRangeFresh = function isRangeFresh () { - var ifRange = this.req.headers['if-range'] + if (!ifRange) { + return true + } - if (!ifRange) { - return true - } + // if-range as etag + if (ifRange.indexOf('"') !== -1) { + var etag = this.res.getHeader('ETag') + return Boolean(etag && ifRange.indexOf(etag) !== -1) + } - // if-range as etag - if (ifRange.indexOf('"') !== -1) { - var etag = this.res.getHeader('ETag') - return Boolean(etag && ifRange.indexOf(etag) !== -1) + // if-range as modified date + var lastModified = this.res.getHeader('Last-Modified') + return parseHttpDate(lastModified) <= parseHttpDate(ifRange) } - // if-range as modified date - var lastModified = this.res.getHeader('Last-Modified') - return parseHttpDate(lastModified) <= parseHttpDate(ifRange) -} - -/** - * Redirect to path. - * - * @param {string} path - * @private - */ + /** + * Redirect to path. + * + * @param {string} path + * @private + */ + redirect (path) { + var res = this.res -SendStream.prototype.redirect = function redirect (path) { - var res = this.res + if (hasListeners(this, 'directory')) { + this.emit('directory', res, path) + return + } - if (hasListeners(this, 'directory')) { - this.emit('directory', res, path) - return - } + if (this.hasTrailingSlash()) { + this.error(403) + return + } - if (this.hasTrailingSlash()) { - this.error(403) - return - } + var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) + var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc)) + + // redirect + res.statusCode = 301 + res.setHeader('Content-Type', 'text/html; charset=UTF-8') + res.setHeader('Content-Length', Buffer.byteLength(doc)) + res.setHeader('Content-Security-Policy', "default-src 'none'") + res.setHeader('X-Content-Type-Options', 'nosniff') + res.setHeader('Location', loc) + res.end(doc) + } + + /** + * Pipe to `res. + * + * @param {Stream} res + * @return {Stream} res + * @api public + */ + pipe (res) { + // root path + var root = this._root + + // references + this.res = res + + // decode the path + var path = decode(this.path) + if (path === -1) { + this.error(400) + return res + } - var loc = encodeUrl(collapseLeadingSlashes(this.path + '/')) - var doc = createHtmlDocument('Redirecting', 'Redirecting to ' + escapeHtml(loc)) - - // redirect - res.statusCode = 301 - res.setHeader('Content-Type', 'text/html; charset=UTF-8') - res.setHeader('Content-Length', Buffer.byteLength(doc)) - res.setHeader('Content-Security-Policy', "default-src 'none'") - res.setHeader('X-Content-Type-Options', 'nosniff') - res.setHeader('Location', loc) - res.end(doc) -} + // null byte(s) + if (~path.indexOf('\0')) { + this.error(400) + return res + } -/** - * Pipe to `res. - * - * @param {Stream} res - * @return {Stream} res - * @api public - */ + var parts + if (root !== null) { + // normalize + if (path) { + path = normalize('.' + sep + path) + } -SendStream.prototype.pipe = function pipe (res) { - // root path - var root = this._root + // malicious path + if (UP_PATH_REGEXP.test(path)) { + debug('malicious path "%s"', path) + this.error(403) + return res + } - // references - this.res = res + // explode path parts + parts = path.split(sep) - // decode the path - var path = decode(this.path) - if (path === -1) { - this.error(400) - return res - } + // join / normalize from optional root dir + path = normalize(join(root, path)) + } else { + // ".." is malicious without "root" + if (UP_PATH_REGEXP.test(path)) { + debug('malicious path "%s"', path) + this.error(403) + return res + } - // null byte(s) - if (~path.indexOf('\0')) { - this.error(400) - return res - } + // explode path parts + parts = normalize(path).split(sep) - var parts - if (root !== null) { - // normalize - if (path) { - path = normalize('.' + sep + path) + // resolve the path + path = resolve(path) } - // malicious path - if (UP_PATH_REGEXP.test(path)) { - debug('malicious path "%s"', path) - this.error(403) - return res + // dotfile handling + if (containsDotFile(parts)) { + debug('%s dotfile "%s"', this._dotfiles, path) + switch (this._dotfiles) { + case 'allow': + break + case 'deny': + this.error(403) + return res + case 'ignore': + default: + this.error(404) + return res + } } - // explode path parts - parts = path.split(sep) - - // join / normalize from optional root dir - path = normalize(join(root, path)) - } else { - // ".." is malicious without "root" - if (UP_PATH_REGEXP.test(path)) { - debug('malicious path "%s"', path) - this.error(403) + // index file support + if (this._index.length && this.hasTrailingSlash()) { + this.sendIndex(path) return res } - // explode path parts - parts = normalize(path).split(sep) - - // resolve the path - path = resolve(path) + this.sendFile(path) + return res } - // dotfile handling - if (containsDotFile(parts)) { - debug('%s dotfile "%s"', this._dotfiles, path) - switch (this._dotfiles) { - case 'allow': - break - case 'deny': - this.error(403) - return res - case 'ignore': - default: - this.error(404) - return res + /** + * Transfer `path`. + * + * @param {String} path + * @api public + */ + send (path, stat) { + var len = stat.size + var options = this.options + var opts = {} + var res = this.res + var req = this.req + var ranges = req.headers.range + var offset = options.start || 0 + + if (res.headersSent) { + // impossible to send now + this.headersAlreadySent() + return } - } - // index file support - if (this._index.length && this.hasTrailingSlash()) { - this.sendIndex(path) - return res - } + debug('pipe "%s"', path) - this.sendFile(path) - return res -} + // set header fields + this.setHeader(path, stat) -/** - * Transfer `path`. - * - * @param {String} path - * @api public - */ + // set content-type + this.type(path) -SendStream.prototype.send = function send (path, stat) { - var len = stat.size - var options = this.options - var opts = {} - var res = this.res - var req = this.req - var ranges = req.headers.range - var offset = options.start || 0 - - if (res.headersSent) { - // impossible to send now - this.headersAlreadySent() - return - } + // conditional GET support + if (this.isConditionalGET()) { + if (this.isPreconditionFailure()) { + this.error(412) + return + } - debug('pipe "%s"', path) + if (this.isCachable() && this.isFresh()) { + this.notModified() + return + } + } - // set header fields - this.setHeader(path, stat) + // adjust len to start/end options + len = Math.max(0, len - offset) + if (options.end !== undefined) { + var bytes = options.end - offset + 1 + if (len > bytes) len = bytes + } - // set content-type - this.type(path) + // Range support + if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { + // parse + ranges = parseRange(len, ranges, { + combine: true + }) - // conditional GET support - if (this.isConditionalGET()) { - if (this.isPreconditionFailure()) { - this.error(412) - return + // If-Range support + if (!this.isRangeFresh()) { + debug('range stale') + ranges = -2 + } + + // unsatisfiable + if (ranges === -1) { + debug('range unsatisfiable') + + // Content-Range + res.setHeader('Content-Range', contentRange('bytes', len)) + + // 416 Requested Range Not Satisfiable + return this.error(416, { + headers: { 'Content-Range': res.getHeader('Content-Range') } + }) + } + + // valid (syntactically invalid/multiple ranges are treated as a regular response) + if (ranges !== -2 && ranges.length === 1) { + debug('range %j', ranges) + + // Content-Range + res.statusCode = 206 + res.setHeader('Content-Range', contentRange('bytes', len, ranges[0])) + + // adjust for requested range + offset += ranges[0].start + len = ranges[0].end - ranges[0].start + 1 + } } - if (this.isCachable() && this.isFresh()) { - this.notModified() - return + // clone options + for (var prop in options) { + opts[prop] = options[prop] } - } - // adjust len to start/end options - len = Math.max(0, len - offset) - if (options.end !== undefined) { - var bytes = options.end - offset + 1 - if (len > bytes) len = bytes - } + // set read options + opts.start = offset + opts.end = Math.max(offset, offset + len - 1) - // Range support - if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { - // parse - ranges = parseRange(len, ranges, { - combine: true - }) + // content-length + res.setHeader('Content-Length', len) - // If-Range support - if (!this.isRangeFresh()) { - debug('range stale') - ranges = -2 + // HEAD support + if (req.method === 'HEAD') { + res.end() + return } - // unsatisfiable - if (ranges === -1) { - debug('range unsatisfiable') - - // Content-Range - res.setHeader('Content-Range', contentRange('bytes', len)) + this.stream(path, opts) + } + + /** + * Transfer file for `path`. + * + * @param {String} path + * @api private + */ + sendFile (path) { + var i = 0 + var self = this + + debug('stat "%s"', path) + fs.stat(path, function onstat (err, stat) { + var pathEndsWithSep = path[path.length - 1] === sep + if (err && err.code === 'ENOENT' && !extname(path) && !pathEndsWithSep) { + // not found, check extensions + return next(err) + } + if (err) return self.onStatError(err) + if (stat.isDirectory()) return self.redirect(path) + if (pathEndsWithSep) return self.error(404) + self.emit('file', path, stat) + self.send(path, stat) + }) - // 416 Requested Range Not Satisfiable - return this.error(416, { - headers: { 'Content-Range': res.getHeader('Content-Range') } + function next (err) { + if (self._extensions.length <= i) { + return err + ? self.onStatError(err) + : self.error(404) + } + + var p = path + '.' + self._extensions[i++] + + debug('stat "%s"', p) + fs.stat(p, function (err, stat) { + if (err) return next(err) + if (stat.isDirectory()) return next() + self.emit('file', p, stat) + self.send(p, stat) }) } + } - // valid (syntactically invalid/multiple ranges are treated as a regular response) - if (ranges !== -2 && ranges.length === 1) { - debug('range %j', ranges) - - // Content-Range - res.statusCode = 206 - res.setHeader('Content-Range', contentRange('bytes', len, ranges[0])) - - // adjust for requested range - offset += ranges[0].start - len = ranges[0].end - ranges[0].start + 1 + /** + * Transfer index for `path`. + * + * @param {String} path + * @api private + */ + sendIndex (path) { + var i = -1 + var self = this + + function next (err) { + if (++i >= self._index.length) { + if (err) return self.onStatError(err) + return self.error(404) + } + + var p = join(path, self._index[i]) + + debug('stat "%s"', p) + fs.stat(p, function (err, stat) { + if (err) return next(err) + if (stat.isDirectory()) return next() + self.emit('file', p, stat) + self.send(p, stat) + }) } - } - // clone options - for (var prop in options) { - opts[prop] = options[prop] + next() } - // set read options - opts.start = offset - opts.end = Math.max(offset, offset + len - 1) - - // content-length - res.setHeader('Content-Length', len) + /** + * Stream `path` to the response. + * + * @param {String} path + * @param {Object} options + * @api private + */ + stream (path, options) { + var self = this + var res = this.res - // HEAD support - if (req.method === 'HEAD') { - res.end() - return - } - - this.stream(path, opts) -} + // pipe + var stream = fs.createReadStream(path, options) + this.emit('stream', stream) + stream.pipe(res) -/** - * Transfer file for `path`. - * - * @param {String} path - * @api private - */ -SendStream.prototype.sendFile = function sendFile (path) { - var i = 0 - var self = this - - debug('stat "%s"', path) - fs.stat(path, function onstat (err, stat) { - var pathEndsWithSep = path[path.length - 1] === sep - if (err && err.code === 'ENOENT' && !extname(path) && !pathEndsWithSep) { - // not found, check extensions - return next(err) - } - if (err) return self.onStatError(err) - if (stat.isDirectory()) return self.redirect(path) - if (pathEndsWithSep) return self.error(404) - self.emit('file', path, stat) - self.send(path, stat) - }) - - function next (err) { - if (self._extensions.length <= i) { - return err - ? self.onStatError(err) - : self.error(404) - } - - var p = path + '.' + self._extensions[i++] - - debug('stat "%s"', p) - fs.stat(p, function (err, stat) { - if (err) return next(err) - if (stat.isDirectory()) return next() - self.emit('file', p, stat) - self.send(p, stat) - }) - } -} + // cleanup + function cleanup () { + destroy(stream, true) + } -/** - * Transfer index for `path`. - * - * @param {String} path - * @api private - */ -SendStream.prototype.sendIndex = function sendIndex (path) { - var i = -1 - var self = this + // response finished, cleanup + onFinished(res, cleanup) - function next (err) { - if (++i >= self._index.length) { - if (err) return self.onStatError(err) - return self.error(404) - } + // error handling + stream.on('error', function onerror (err) { + // clean up stream early + cleanup() - var p = join(path, self._index[i]) + // error + self.onStatError(err) + }) - debug('stat "%s"', p) - fs.stat(p, function (err, stat) { - if (err) return next(err) - if (stat.isDirectory()) return next() - self.emit('file', p, stat) - self.send(p, stat) + // end + stream.on('end', function onend () { + self.emit('end') }) } - next() -} - -/** - * Stream `path` to the response. - * - * @param {String} path - * @param {Object} options - * @api private - */ + /** + * Set content-type based on `path` + * if it hasn't been explicitly set. + * + * @param {String} path + * @api private + */ + type (path) { + var res = this.res -SendStream.prototype.stream = function stream (path, options) { - var self = this - var res = this.res + if (res.getHeader('Content-Type')) return - // pipe - var stream = fs.createReadStream(path, options) - this.emit('stream', stream) - stream.pipe(res) + var ext = extname(path) + var type = mime.contentType(ext) || 'application/octet-stream' - // cleanup - function cleanup () { - destroy(stream, true) + debug('content-type %s', type) + res.setHeader('Content-Type', type) } - // response finished, cleanup - onFinished(res, cleanup) + /** + * Set response header fields, most + * fields may be pre-defined. + * + * @param {String} path + * @param {Object} stat + * @api private + */ + setHeader (path, stat) { + var res = this.res - // error handling - stream.on('error', function onerror (err) { - // clean up stream early - cleanup() + this.emit('headers', res, path, stat) - // error - self.onStatError(err) - }) - - // end - stream.on('end', function onend () { - self.emit('end') - }) -} - -/** - * Set content-type based on `path` - * if it hasn't been explicitly set. - * - * @param {String} path - * @api private - */ - -SendStream.prototype.type = function type (path) { - var res = this.res - - if (res.getHeader('Content-Type')) return - - var ext = extname(path) - var type = mime.contentType(ext) || 'application/octet-stream' - - debug('content-type %s', type) - res.setHeader('Content-Type', type) -} - -/** - * Set response header fields, most - * fields may be pre-defined. - * - * @param {String} path - * @param {Object} stat - * @api private - */ - -SendStream.prototype.setHeader = function setHeader (path, stat) { - var res = this.res - - this.emit('headers', res, path, stat) + if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { + debug('accept ranges') + res.setHeader('Accept-Ranges', 'bytes') + } - if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { - debug('accept ranges') - res.setHeader('Accept-Ranges', 'bytes') - } + if (this._cacheControl && !res.getHeader('Cache-Control')) { + var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) - if (this._cacheControl && !res.getHeader('Cache-Control')) { - var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) + if (this._immutable) { + cacheControl += ', immutable' + } - if (this._immutable) { - cacheControl += ', immutable' + debug('cache-control %s', cacheControl) + res.setHeader('Cache-Control', cacheControl) } - debug('cache-control %s', cacheControl) - res.setHeader('Cache-Control', cacheControl) - } - - if (this._lastModified && !res.getHeader('Last-Modified')) { - var modified = stat.mtime.toUTCString() - debug('modified %s', modified) - res.setHeader('Last-Modified', modified) - } + if (this._lastModified && !res.getHeader('Last-Modified')) { + var modified = stat.mtime.toUTCString() + debug('modified %s', modified) + res.setHeader('Last-Modified', modified) + } - if (this._etag && !res.getHeader('ETag')) { - var val = etag(stat) - debug('etag %s', val) - res.setHeader('ETag', val) + if (this._etag && !res.getHeader('ETag')) { + var val = etag(stat) + debug('etag %s', val) + res.setHeader('ETag', val) + } } }