UNPKG

file-send

Version:
1,561 lines (1,321 loc) 33.1 kB
/** * @module file-send * @author nuintun * @license MIT * @version 4.0.3 * @description A http file send. * @see https://github.com/nuintun/file-send#readme */ 'use strict'; const etag$1 = require('etag'); const fs = require('fs'); const fresh = require('fresh'); const http = require('http'); const destroy = require('destroy'); const Stream = require('stream'); const Events = require('events'); const encodeUrl = require('encodeurl'); const mime = require('mime-types'); const path$1 = require('path'); const micromatch = require('micromatch'); const escapeHtml = require('escape-html'); const parseRange$1 = require('range-parser'); const ms = require('ms'); /** * @module utils * @license MIT * @author nuintun */ const undef = void 0; const toString = Object.prototype.toString; const CHARS = Array.from('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'); /** * @function typeOf * @description The data type judgment * @param {any} value * @param {string} type * @returns {boolean} */ function typeOf(value, type) { // Format type type = String(type).toLowerCase(); // Switch switch (type) { case 'nan': return Number.isNaN(value); case 'null': return value === null; case 'array': return Array.isArray(value); case 'function': return typeof value === 'function'; case 'undefined': return value === undef; default: // Get real type const realType = toString.call(value).toLowerCase(); // Is other return realType === '[object ' + type + ']'; } } /** * @function isOutBound * @description Test path is out of bound of base * @param {string} path * @param {string} root * @returns {boolean} */ function isOutBound(path, root) { path = path$1.relative(root, path); return /\.\.(?:[\\/]|$)/.test(path) || path$1.isAbsolute(path); } /** * @function unixify * @description Convert path separators to posix/unix-style forward slashes * @param {string} path * @returns {string} */ function unixify(path) { return path.replace(/\\/g, '/'); } /** * @function normalize * @description Normalize path * @param {string} path * @returns {string} */ function normalize(path) { // \a\b\.\c\.\d ==> /a/b/./c/./d path = unixify(path); // :///a/b/c ==> ://a/b/c path = path.replace(/:\/{3,}/, '://'); // /a/b/./c/./d ==> /a/b/c/d path = path.replace(/\/\.\//g, '/'); // a//b/c ==> a/b/c // //a/b/c ==> /a/b/c // a///b/////c ==> a/b/c path = path.replace(/(^|[^:])\/{2,}/g, '$1/'); // Transfer path let src = path; // DOUBLE_DOT_RE matches a/b/c//../d path correctly only if replace // with / first const DOUBLE_DOT_RE = /([^/]+)\/\.\.(?:\/|$)/g; // a/b/c/../../d ==> a/b/../d ==> a/d do { src = src.replace(DOUBLE_DOT_RE, (matched, dirname) => { return dirname === '..' ? matched : ''; }); // Break if (path === src) { break; } else { path = src; } } while (true); // Get path return path; } /** * @function decodeURI * @description Decode URI component. * @param {string} uri * @returns {string|-1} */ function decodeURI(uri) { try { return decodeURIComponent(uri); } catch (err) { return -1; } } /** * @function boundaryGenerator * @description Create boundary * @returns {string} */ function boundaryGenerator() { let boundary = ''; // Create boundary for (let i = 0; i < 38; i++) { boundary += CHARS[Math.floor(Math.random() * 62)]; } // Return boundary return boundary; } /** * @function parseHttpDate * @description Parse an HTTP Date into a number. * @param {string} date * @private */ function parseHttpDate(date) { const timestamp = date && Date.parse(date); return typeOf(timestamp, 'number') ? timestamp : NaN; } /** * @function pipeline * @param {Stream} streams * @return {Stream} */ function pipeline(streams) { let index = 0; let src = streams[index++]; const length = streams.length; while (index < length) { let dest = streams[index++]; // Listening error event src.once('error', error => { dest.emit('error', error); }); src = src.pipe(dest); } return src; } /** * @function parseTokenList * @description Parse a HTTP token list. * @param {string} value */ function parseTokenList(value) { let end = 0; let list = []; let start = 0; // gather tokens for (let i = 0, length = value.length; i < length; i++) { switch (value.charCodeAt(i)) { case 0x20: // ' ' if (start === end) { start = end = i + 1; } break; case 0x2c: // ',' list.push(value.substring(start, end)); start = end = i + 1; break; default: end = i + 1; break; } } // final token list.push(value.substring(start, end)); return list; } /** * @function createErrorDocument * @param {number} statusCode * @param {string} statusMessage * @returns {string} */ function createErrorDocument(statusCode, statusMessage) { return ( '<!DOCTYPE html>\n' + '<html>\n' + ' <head>\n' + ' <meta name="renderer" content="webkit" />\n' + ' <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />\n' + ' <meta content="text/html; charset=utf-8" http-equiv="content-type" />\n' + ` <title>${statusCode}</title>\n` + ' <style>\n' + ' html, body, div, p {\n' + ' text-align: center;\n' + ' margin: 0; padding: 0;\n' + ' font-family: Calibri, "Lucida Console", Consolas, "Liberation Mono", Menlo, Courier, monospace;\n' + ' }\n' + ' body { padding-top: 88px; }\n' + ' p { color: #0e90d2; line-height: 100%; }\n' + ' .ui-code { font-size: 200px; font-weight: bold; }\n' + ' .ui-message { font-size: 80px; }\n' + ' </style>\n' + ' </head>\n' + ' <body>\n' + ` <p class="ui-code">${statusCode}</p>\n` + ` <p class="ui-message">${statusMessage}</p>\n` + ' </body>\n' + '</html>\n' ); } /** * @module through * @license MIT * @author nuintun */ /** * @function noop * @description A noop _transform function * @param {any} chunk * @param {string} encoding * @param {Function} next */ function noop(chunk, encoding, next) { next(null, chunk); } /** * @function through * @param {Object} options * @param {Function} transform * @param {Function} flush * @returns {Transform} */ function through(options, transform, flush, destroy) { if (typeOf(options, 'function')) { flush = transform; transform = options; options = {}; } else if (!typeOf(transform, 'function')) { transform = noop; } if (!typeOf(flush, 'function')) flush = null; if (!typeOf(destroy, 'function')) destroy = null; options = options || {}; if (typeOf(options.objectMode, 'undefined')) { options.objectMode = true; } if (typeOf(options.highWaterMark, 'undefined')) { options.highWaterMark = 16; } const stream = new Stream.Transform(options); stream._transform = transform; if (flush) stream._flush = flush; if (destroy) stream._destroy = destroy; return stream; } /** * @module async * @license MIT * @author nuintun */ /** * @class Iterator */ class Iterator { /** * @constructor * @param {Array} array */ constructor(array) { this.index = 0; this.array = Array.isArray(array) ? array : []; } /** * @method next * @description Create the next item. * @returns {{done: boolean, value: undefined}} */ next() { const done = this.index >= this.array.length; const value = !done ? this.array[this.index++] : undefined; return { done: done, value: value }; } } /** * @function series * @param {Array} array * @param {Function} iterator * @param {Function} done * @param {any} context */ function series(array, iterator, done) { // Create a new iterator const it = new Iterator(array); /** * @function walk * @param it */ function walk(it) { const item = it.next(); if (item.done) { done(); } else { iterator(item.value, () => walk(it), it.index); } } // Run walk walk(it); } /** * @module symbol * @license MIT * @author nuintun */ const dir = Symbol('dir'); const etag = Symbol('etag'); const path = Symbol('path'); const root = Symbol('root'); const glob = Symbol('glob'); const stdin = Symbol('stdin'); const error = Symbol('error'); const index = Symbol('index'); const ignore = Symbol('ignore'); const maxAge = Symbol('maxAge'); const charset = Symbol('charset'); const request = Symbol('request'); const isFresh = Symbol('isFresh'); const realpath = Symbol('realpath'); const response = Symbol('response'); const isIgnore = Symbol('isIgnore'); const sendFile = Symbol('sendFile'); const immutable = Symbol('immutable'); const sendIndex = Symbol('sendIndex'); const bootstrap = Symbol('bootstrap'); const statError = Symbol('statError'); const isCachable = Symbol('isCachable'); const parseRange = Symbol('parseRange'); const initHeaders = Symbol('initHeaders'); const responseEnd = Symbol('responseEnd'); const headersSent = Symbol('headersSent'); const middlewares = Symbol('middlewares'); const isRangeFresh = Symbol('isRangeFresh'); const ignoreAccess = Symbol('ignoreAccess'); const acceptRanges = Symbol('acceptRanges'); const cacheControl = Symbol('cacheControl'); const lastModified = Symbol('lastModified'); const hasTrailingSlash = Symbol('hasTrailingSlash'); const isConditionalGET = Symbol('isConditionalGET'); const isPreconditionFailure = Symbol('isPreconditionFailure'); /** * @module normalize * @license MIT * @author nuintun */ // Current working directory const CWD = process.cwd(); // The max max-age set const MAX_MAX_AGE = 60 * 60 * 24 * 365; /** * @function normalizeCharset * @param {string} charset * @returns {string|null} */ function normalizeCharset(charset) { return charset && typeOf(charset, 'string') ? charset : null; } /** * @function normalizeRoot * @param {string} root * @returns {string} */ function normalizeRoot(root) { return unixify(typeOf(root, 'string') ? path$1.resolve(root) : CWD); } /** * @function normalizePath * @param {string} path * @returns {string|-1} */ function normalizePath(path) { path = decodeURI(path); return path === -1 ? path : normalize(path); } /** * @function normalizeRealpath * @param {string} root * @param {string} path * @returns {string|-1} */ function normalizeRealpath(root, path) { return path === -1 ? path : unixify(path$1.join(root, path)); } /** * @function normalizeList * @param {Array} list * @returns {Array} */ function normalizeList(list) { list = Array.isArray(list) ? list : [list]; return list.filter(item => item && typeOf(item, 'string')); } /** * @function normalizeAccess * @param {string} access * @returns {string} */ function normalizeAccess(access) { return access === 'ignore' ? access : 'deny'; } /** * @function normalizeMaxAge * @param {string|number} maxAge * @returns {number} */ function normalizeMaxAge(maxAge) { maxAge = typeOf(maxAge, 'string') ? ms(maxAge) / 1000 : Number(maxAge); maxAge = !isNaN(maxAge) ? Math.min(Math.max(0, maxAge), MAX_MAX_AGE) : 0; return Math.floor(maxAge); } /** * @function normalizeBoolean * @param {boolean} boolean * @param {boolean} def * @returns {boolean} */ function normalizeBoolean(boolean, def) { return typeOf(boolean, 'undefined') ? def : Boolean(boolean); } /** * @function normalizeGlob * @param {Object} glob * @returns {string} */ function normalizeGlob(glob) { glob = glob || {}; glob.dot = normalizeBoolean(glob.dot, true); return glob; } /** * @module file-send * @license MIT * @author nuintun */ // File not found status const NOT_FOUND = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; /** * @class FileSend */ class FileSend extends Events { /** * @constructor * @param {Request} request * @param {String} path * @param {Object} options */ constructor(request$1, path, options) { if (!(request$1 instanceof http.IncomingMessage)) { throw new TypeError('The param request must be a http request.'); } if (!typeOf(path, 'string')) { throw new TypeError('The param path must be a string.'); } super(options); this.path = path; this.root = options.root; this.index = options.index; this.ignore = options.ignore; this.maxAge = options.maxAge; this.charset = options.charset; this.etag = options.etag; this.ignoreAccess = options.ignoreAccess; this.immutable = options.immutable; this.acceptRanges = options.acceptRanges; this.cacheControl = options.cacheControl; this.lastModified = options.lastModified; this[middlewares] = []; this[request] = request$1; this[stdin] = through(); this[glob] = normalizeGlob(options.glob); } /** * @property request * @method get */ get request() { return this[request]; } /** * @property response * @method get */ get response() { const response$1 = this[response]; if (!response$1) { throw new ReferenceError("Can't get http response before called pipe method."); } return response$1; } /** * @property method * @method get */ get method() { return this.request.method; } /** * @property path * @method set */ set path(path$1) { const root = this.root; path$1 = normalizePath(path$1); this[path] = path$1; this[realpath] = root ? normalizeRealpath(root, path$1) : path$1; } /** * @property path * @method get */ get path() { return this[path]; } /** * @property root * @method set */ set root(root$1) { const path = this.path; root$1 = normalizeRoot(root$1); this[root] = root$1; this[realpath] = path ? normalizeRealpath(root$1, path) : root$1; } /** * @property root * @method get */ get root() { return this[root]; } /** * @property realpath * @method get */ get realpath() { return this[realpath]; } /** * @property index * @method set */ set index(index$1) { this[index] = normalizeList(index$1); } /** * @property index * @method get */ get index() { return this[index]; } /** * @property ignore * @method set */ set ignore(ignore$1) { this[ignore] = normalizeList(ignore$1); } /** * @property ignore * @method get */ get ignore() { return this[ignore]; } /** * @property ignoreAccess * @method set */ set ignoreAccess(ignoreAccess$1) { this[ignoreAccess] = normalizeAccess(ignoreAccess$1); } /** * @property ignoreAccess * @method get */ get ignoreAccess() { return this[ignoreAccess]; } /** * @property maxAge * @method set */ set maxAge(maxAge$1) { this[maxAge] = normalizeMaxAge(maxAge$1); } /** * @property maxAge * @method get */ get maxAge() { return this[maxAge]; } /** * @property charset * @method set */ set charset(charset$1) { this[charset] = normalizeCharset(charset$1); } /** * @property charset * @method get */ get charset() { return this[charset]; } /** * @property etag * @method set */ set etag(etag$1) { this[etag] = normalizeBoolean(etag$1, true); } /** * @property etag * @method get */ get etag() { return this[etag]; } /** * @property immutable * @method set */ set immutable(immutable$1) { this[immutable] = normalizeBoolean(immutable$1, false); } /** * @property immutable * @method get */ get immutable() { return this[immutable]; } /** * @property acceptRanges * @method set */ set acceptRanges(acceptRanges$1) { this[acceptRanges] = normalizeBoolean(acceptRanges$1, true); } /** * @property acceptRanges * @method get */ get acceptRanges() { return this[acceptRanges]; } /** * @property cacheControl * @method set */ set cacheControl(cacheControl$1) { this[cacheControl] = normalizeBoolean(cacheControl$1, true); } /** * @property cacheControl * @method get */ get cacheControl() { return this[cacheControl]; } /** * @property lastModified * @method set */ set lastModified(lastModified$1) { this[lastModified] = normalizeBoolean(lastModified$1, true); } /** * @property lastModified * @method get */ get lastModified() { return this[lastModified]; } /** * @property statusCode * @method set */ set statusCode(statusCode) { this.response.statusCode = statusCode; } /** * @property statusCode * @method get */ get statusCode() { return this.response.statusCode; } /** * @property statusMessage * @method set */ set statusMessage(statusMessage) { this.response.statusMessage = statusMessage || http.STATUS_CODES[this.statusCode]; } /** * @property statusMessage * @method get */ get statusMessage() { return this.response.statusMessage; } /** * @method use * @param {Stream} middleware * @public */ use(middleware) { if (middleware instanceof Stream) { this[middlewares].push(middleware); } return this; } /** * @method setHeader * @param {string} name * @param {string} value * @public */ setHeader(name, value) { this.response.setHeader(name, value); } /** * @method getHeader * @param {string} name * @public */ getHeader(name) { return this.response.getHeader(name); } /** * @method removeHeader * @param {string} name * @public */ removeHeader(name) { this.response.removeHeader(name); } /** * @method removeHeaders * @public */ hasHeader(name) { return this.response.hasHeader(name); } /** * @method hasListeners * @param {string} event * @public */ hasListeners(event) { return this.listenerCount(event) > 0; } /** * @method status * @param {number} statusCode * @param {string} statusMessage * @public */ status(statusCode, statusMessage) { this.statusCode = statusCode; this.statusMessage = statusMessage; } /** * @method redirect * @param {string} location * @public */ redirect(location) { const href = encodeUrl(location); const html = `Redirecting to <a href="${href}">${escapeHtml(location)}</a>`; this.status(301); this.setHeader('Cache-Control', 'no-cache'); this.setHeader('Content-Type', 'text/html; charset=UTF-8'); this.setHeader('Content-Length', Buffer.byteLength(html)); this.setHeader('Content-Security-Policy', "default-src 'self'"); this.setHeader('X-Content-Type-Options', 'nosniff'); this.setHeader('Location', href); this[responseEnd](html); } /** * @method pipe * @param {Response} response * @param {Object} options * @public */ pipe(response$1, options) { if (this[response]) { throw new RangeError('The pipe method has been called more than once.'); } if (!(response$1 instanceof http.ServerResponse)) { throw new TypeError('The response must be a http response.'); } // Set response this[response] = response$1; // Headers already sent if (response$1.headersSent) { this[headersSent](); return response$1; } // Listening error event response$1.once('error', error => { this[statError](error); }); // Bootstrap this[bootstrap](); // Pipeline const streams = [this[stdin]].concat(this[middlewares]); streams.push(response$1); return pipeline(streams); } /** * @method end * @param {string} chunk * @param {string} encoding * @param {Function} callback * @public */ end(chunk, encoding, callback) { if (chunk) { this[stdin].end(chunk, encoding, callback); } else { this[stdin].end(); } } /** * @method headersSent * @private */ [headersSent]() { this[responseEnd]("Can't set headers after they are sent."); } /** * @method error * @param {number} statusCode * @param {string} statusMessage * @public */ [error](statusCode, statusMessage) { const response = this.response; this.status(statusCode, statusMessage); statusCode = this.statusCode; statusMessage = this.statusMessage; const error = new Error(statusMessage); error.statusCode = statusCode; // Emit if listeners instead of responding if (this.hasListeners('error')) { this.emit('error', error, chunk => { if (response.headersSent) { return this[headersSent](); } this[responseEnd](chunk); }); } else { if (response.headersSent) { return this[headersSent](); } // Error document const document = createErrorDocument(statusCode, statusMessage); // Set headers this.setHeader('Cache-Control', 'private'); this.setHeader('Content-Type', 'text/html; charset=UTF-8'); this.setHeader('Content-Length', Buffer.byteLength(document)); this.setHeader('Content-Security-Policy', `default-src 'self' 'unsafe-inline'`); this.setHeader('X-Content-Type-Options', 'nosniff'); this[responseEnd](document); } } /** * @method statError * @param {Error} error * @private */ [statError](error$1) { // 404 error if (NOT_FOUND.indexOf(error$1.code) !== -1) { return this[error](404); } this[error](500, error$1.message); } /** * @method hasTrailingSlash * @private */ [hasTrailingSlash]() { return this.path[this.path.length - 1] === '/'; } /** * @method isConditionalGET * @private */ [isConditionalGET]() { const headers = this.request.headers; return headers['if-match'] || headers['if-unmodified-since'] || headers['if-none-match'] || headers['if-modified-since']; } /** * @method isPreconditionFailure * @private */ [isPreconditionFailure]() { const request = this.request; // if-match const match = request.headers['if-match']; if (match) { const etag = this.getHeader('ETag'); return ( !etag || (match !== '*' && parseTokenList(match).every(match => { return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag; })) ); } // if-unmodified-since const unmodifiedSince = parseHttpDate(request.headers['if-unmodified-since']); if (!isNaN(unmodifiedSince)) { const lastModified = parseHttpDate(this.getHeader('Last-Modified')); return isNaN(lastModified) || lastModified > unmodifiedSince; } return false; } /** * @method isCachable * @private */ [isCachable]() { const statusCode = this.statusCode; return statusCode === 304 || (statusCode >= 200 && statusCode < 300); } /** * @method isFresh * @private */ [isFresh]() { return fresh(this.request.headers, { etag: this.getHeader('ETag'), 'last-modified': this.getHeader('Last-Modified') }); } /** * @method isRangeFresh * @private */ [isRangeFresh]() { const ifRange = this.request.headers['if-range']; if (!ifRange) { return true; } // If-Range as etag if (ifRange.indexOf('"') !== -1) { const etag = this.getHeader('ETag'); return Boolean(etag && ifRange.indexOf(etag) !== -1); } // If-Range as modified date const lastModified = this.getHeader('Last-Modified'); return parseHttpDate(lastModified) <= parseHttpDate(ifRange); } /** * @method isIgnore * @param {string} path * @private */ [isIgnore](path) { return this.ignore.length && micromatch.isMatch(path, this.ignore, this[glob]); } /** * @method parseRange * @param {Stats} stats * @private */ [parseRange](stats) { const result = []; const size = stats.size; let contentLength = size; // Range support if (this.acceptRanges) { let ranges = this.request.headers['range']; // Range fresh if (ranges && this[isRangeFresh]()) { // Parse range -1 -2 or [] ranges = parseRange$1(size, ranges, { combine: true }); // Valid ranges, support multiple ranges if (Array.isArray(ranges) && ranges.type === 'bytes') { this.status(206); // Multiple ranges if (ranges.length > 1) { // Range boundary let boundary = `<${boundaryGenerator()}>`; // If user set content-type use user define const contentType = this.getHeader('Content-Type') || 'application/octet-stream'; // Set multipart/byteranges this.setHeader('Content-Type', `multipart/byteranges; boundary=${boundary}`); // Create boundary and end boundary boundary = `\r\n--${boundary}`; // Closed boundary const close = `${boundary}--\r\n`; // Common boundary boundary += `\r\nContent-Type: ${contentType}`; // Reset content-length contentLength = 0; // Map ranges ranges.forEach(range => { // Range start and end const start = range.start; const end = range.end; // Set fields const open = `${boundary}\r\nContent-Range: bytes ${start}-${end}/${size}\r\n\r\n`; // Set property range.open = open; // Compute content-length contentLength += end - start + Buffer.byteLength(open) + 1; // Cache range result.push(range); }); // The first open boundary remove \r\n result[0].open = result[0].open.replace(/^\r\n/, ''); // The last add closed boundary result[result.length - 1].close = close; // Compute content-length contentLength += Buffer.byteLength(close); } else { const range = ranges[0]; const start = range.start; const end = range.end; // Set content-range this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`); // Cache range result.push(range); // Compute content-length contentLength = end - start + 1; } } else if (ranges === -1) { return ranges; } } } // Set content-length this.setHeader('Content-Length', contentLength); // If non range return all file if (!result.length) { result.push({}); } // Return result return result; } /** * @method dir * @private */ [dir]() { // If have event directory listener, use user define // emit event directory if (this.hasListeners('dir')) { this.emit('dir', this.realpath, chunk => { if (this.response.headersSent) { return this[headersSent](); } this[responseEnd](chunk); }); } else { this[error](403); } } /** * @method initHeaders * @param {Stats} stats * @private */ [initHeaders](stats) { // Accept-Ranges if (this.acceptRanges) { // Set Accept-Ranges this.setHeader('Accept-Ranges', 'bytes'); } // Content-Type if (!this.hasHeader('Content-Type')) { // Get type let type = mime.lookup(this.path); if (type) { let charset = this.charset; // Get charset charset = charset ? `; charset=${charset}` : ''; // Set Content-Type this.setHeader('Content-Type', type + charset); } } // Cache-Control if (this.cacheControl && !this.hasHeader('Cache-Control')) { let cacheControl = `public, max-age=${this.maxAge}`; if (this.immutable) { cacheControl += ', immutable'; } // Set Cache-Control this.setHeader('Cache-Control', cacheControl); } // Last-Modified if (this.lastModified && !this.hasHeader('Last-Modified')) { // Get mtime utc string this.setHeader('Last-Modified', stats.mtime.toUTCString()); } if (this.etag && !this.hasHeader('ETag')) { // Set ETag this.setHeader('ETag', etag$1(stats)); } } /** * @method responseEnd * @param {string} chunk * @param {string} chunk * @param {Function} chunk * @private */ [responseEnd](chunk, encoding, callback) { const response = this.response; if (response) { if (chunk) { response.end(chunk, encoding, callback); } else { response.end(); } } // Destroy stdin stream destroy(this[stdin]); } /** * @method sendIndex * @private */ [sendIndex]() { const hasTrailingSlash$1 = this[hasTrailingSlash](); const path = hasTrailingSlash$1 ? this.path : `${this.path}/`; // Iterator index series( this.index.map(index => path + index), (path, next) => { if (this[isIgnore](path)) { return next(); } fs.stat(this.root + path, (error, stats) => { if (error || !stats.isFile()) { return next(); } this.redirect(unixify(path)); }); }, () => { if (hasTrailingSlash$1) { return this[dir](); } this.redirect(path); } ); } /** * @method sendFile * @private */ [sendFile](ranges) { const realpath = this.realpath; const stdin$1 = this[stdin]; // Iterator ranges series( ranges, (range, next) => { // Write open boundary range.open && stdin$1.write(range.open); // Create file stream const file = fs.createReadStream(realpath, range); // Error handling code-smell file.on('error', error => { // Emit stdin error stdin$1.emit('error', error); // Destroy file stream destroy(file); }); // File stream end file.on('end', () => { // Stop pipe stdin file.unpipe(stdin$1); // Push close boundary range.close && stdin$1.write(range.close); // Destroy file stream destroy(file); }); // Next file.on('close', next); // Pipe stdin file.pipe(stdin$1, { end: false }); }, // End stdin () => this.end() ); } /** * @method bootstrap * @private */ [bootstrap]() { const method = this.method; const realpath = this.realpath; // Only support GET and HEAD if (method !== 'GET' && method !== 'HEAD') { // End with empty content return this[error](405); } // Path -1 or null byte(s) if (this.path === -1 || this.path.indexOf('\0') !== -1) { return this[error](400); } // Malicious path if (isOutBound(realpath, this.root)) { return this[error](403); } // Is ignore path or file if (this[isIgnore](this.path)) { switch (this.ignoreAccess) { case 'deny': return this[error](403); case 'ignore': return this[error](404); } } // Set status this.status(200); // Read file fs.stat(realpath, (error$1, stats) => { // Stat error if (error$1) { return this[statError](error$1); } // Is directory if (stats.isDirectory()) { return this[sendIndex](); } else if (this[hasTrailingSlash]()) { // Not a directory but has trailing slash return this[error](404); } // Set headers and parse range this[initHeaders](stats); // Conditional get support if (this[isConditionalGET]()) { if (this[isPreconditionFailure]()) { return this[error](412); } if (this[isCachable]() && this[isFresh]()) { this.status(304); this.removeHeader('Content-Type'); return this[responseEnd](); } } // Head request if (method === 'HEAD') { // Set content-length this.setHeader('Content-Length', stats.size); // End with empty content return this[responseEnd](); } // Parse ranges const ranges = this[parseRange](stats); // 416 if (ranges === -1) { // Set content-range this.setHeader('Content-Range', `bytes */${stats.size}`); // Unsatisfiable 416 this[error](416); } else { // Emit file event if (this.hasListeners('file')) { this.emit('file', realpath, stats); } // Read file this[sendFile](ranges); } }); } } // Exports mime FileSend.mime = mime; module.exports = FileSend;