Buckets:
| /*! | |
| * send | |
| * Copyright(c) 2012 TJ Holowaychuk | |
| * Copyright(c) 2014-2022 Douglas Christopher Wilson | |
| * MIT Licensed | |
| */ | |
| /** | |
| * Module dependencies. | |
| * @private | |
| */ | |
| var createError = require('http-errors') | |
| var debug = require('debug')('send') | |
| var encodeUrl = require('encodeurl') | |
| var escapeHtml = require('escape-html') | |
| var etag = require('etag') | |
| var fresh = require('fresh') | |
| var fs = require('fs') | |
| var mime = require('mime-types') | |
| var ms = require('ms') | |
| var onFinished = require('on-finished') | |
| var parseRange = require('range-parser') | |
| var path = require('path') | |
| var statuses = require('statuses') | |
| var Stream = require('stream') | |
| var util = require('util') | |
| /** | |
| * Path function references. | |
| * @private | |
| */ | |
| var extname = path.extname | |
| var join = path.join | |
| var normalize = path.normalize | |
| var resolve = path.resolve | |
| var sep = path.sep | |
| /** | |
| * Regular expression for identifying a bytes Range header. | |
| * @private | |
| */ | |
| var BYTES_RANGE_REGEXP = /^ *bytes=/ | |
| /** | |
| * Maximum value allowed for the max age. | |
| * @private | |
| */ | |
| var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year | |
| /** | |
| * Regular expression to match a path with a directory up component. | |
| * @private | |
| */ | |
| var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ | |
| /** | |
| * Module exports. | |
| * @public | |
| */ | |
| module.exports = send | |
| /** | |
| * Return a `SendStream` for `req` and `path`. | |
| * | |
| * @param {object} req | |
| * @param {string} path | |
| * @param {object} [options] | |
| * @return {SendStream} | |
| * @public | |
| */ | |
| function send (req, path, options) { | |
| return new SendStream(req, path, options) | |
| } | |
| /** | |
| * Initialize a `SendStream` with the given `path`. | |
| * | |
| * @param {Request} req | |
| * @param {String} path | |
| * @param {object} [options] | |
| * @private | |
| */ | |
| function SendStream (req, path, options) { | |
| Stream.call(this) | |
| var opts = options || {} | |
| this.options = opts | |
| this.path = path | |
| this.req = req | |
| this._acceptRanges = opts.acceptRanges !== undefined | |
| ? Boolean(opts.acceptRanges) | |
| : true | |
| this._cacheControl = opts.cacheControl !== undefined | |
| ? Boolean(opts.cacheControl) | |
| : true | |
| this._etag = opts.etag !== undefined | |
| ? Boolean(opts.etag) | |
| : true | |
| 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) | |
| } | |
| // 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 | |
| */ | |
| SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () { | |
| return this.path[this.path.length - 1] === '/' | |
| } | |
| /** | |
| * Check if this is a conditional GET request. | |
| * | |
| * @return {Boolean} | |
| * @api private | |
| */ | |
| 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'] | |
| } | |
| /** | |
| * Check if the request preconditions failed. | |
| * | |
| * @return {boolean} | |
| * @private | |
| */ | |
| 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 | |
| })) | |
| } | |
| // 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 | |
| } | |
| 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 | |
| */ | |
| 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 | |
| */ | |
| SendStream.prototype.isRangeFresh = function isRangeFresh () { | |
| var ifRange = this.req.headers['if-range'] | |
| 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 modified date | |
| var lastModified = this.res.getHeader('Last-Modified') | |
| return parseHttpDate(lastModified) <= parseHttpDate(ifRange) | |
| } | |
| /** | |
| * Redirect to path. | |
| * | |
| * @param {string} path | |
| * @private | |
| */ | |
| SendStream.prototype.redirect = function redirect (path) { | |
| var res = this.res | |
| if (hasListeners(this, 'directory')) { | |
| this.emit('directory', res, path) | |
| 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 | |
| */ | |
| SendStream.prototype.pipe = function 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 | |
| } | |
| // null byte(s) | |
| if (~path.indexOf('\0')) { | |
| this.error(400) | |
| return res | |
| } | |
| var parts | |
| if (root !== null) { | |
| // normalize | |
| if (path) { | |
| path = normalize('.' + sep + path) | |
| } | |
| // malicious path | |
| if (UP_PATH_REGEXP.test(path)) { | |
| debug('malicious path "%s"', path) | |
| this.error(403) | |
| 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) | |
| return res | |
| } | |
| // explode path parts | |
| parts = normalize(path).split(sep) | |
| // resolve the path | |
| path = resolve(path) | |
| } | |
| // 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 | |
| } | |
| } | |
| // index file support | |
| if (this._index.length && this.hasTrailingSlash()) { | |
| this.sendIndex(path) | |
| return res | |
| } | |
| this.sendFile(path) | |
| return res | |
| } | |
| /** | |
| * Transfer `path`. | |
| * | |
| * @param {String} path | |
| * @api public | |
| */ | |
| 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 | |
| } | |
| debug('pipe "%s"', path) | |
| // set header fields | |
| this.setHeader(path, stat) | |
| // set content-type | |
| this.type(path) | |
| // conditional GET support | |
| if (this.isConditionalGET()) { | |
| if (this.isPreconditionFailure()) { | |
| this.error(412) | |
| return | |
| } | |
| if (this.isCachable() && this.isFresh()) { | |
| this.notModified() | |
| return | |
| } | |
| } | |
| // 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 | |
| } | |
| // Range support | |
| if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) { | |
| // parse | |
| ranges = parseRange(len, ranges, { | |
| combine: true | |
| }) | |
| // 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 | |
| } | |
| } | |
| // clone options | |
| for (var prop in options) { | |
| opts[prop] = options[prop] | |
| } | |
| // set read options | |
| opts.start = offset | |
| opts.end = Math.max(offset, offset + len - 1) | |
| // content-length | |
| res.setHeader('Content-Length', len) | |
| // HEAD support | |
| if (req.method === 'HEAD') { | |
| res.end() | |
| return | |
| } | |
| this.stream(path, opts) | |
| } | |
| /** | |
| * 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) | |
| }) | |
| } | |
| } | |
| /** | |
| * Transfer index for `path`. | |
| * | |
| * @param {String} path | |
| * @api private | |
| */ | |
| SendStream.prototype.sendIndex = function 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) | |
| }) | |
| } | |
| next() | |
| } | |
| /** | |
| * Stream `path` to the response. | |
| * | |
| * @param {String} path | |
| * @param {Object} options | |
| * @api private | |
| */ | |
| SendStream.prototype.stream = function stream (path, options) { | |
| var self = this | |
| var res = this.res | |
| // pipe | |
| var stream = fs.createReadStream(path, options) | |
| this.emit('stream', stream) | |
| stream.pipe(res) | |
| // cleanup | |
| function cleanup () { | |
| stream.destroy() | |
| } | |
| // response finished, cleanup | |
| onFinished(res, cleanup) | |
| // error handling | |
| stream.on('error', function onerror (err) { | |
| // clean up stream early | |
| cleanup() | |
| // 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._cacheControl && !res.getHeader('Cache-Control')) { | |
| var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) | |
| if (this._immutable) { | |
| cacheControl += ', immutable' | |
| } | |
| 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._etag && !res.getHeader('ETag')) { | |
| var val = etag(stat) | |
| debug('etag %s', val) | |
| res.setHeader('ETag', val) | |
| } | |
| } | |
| /** | |
| * Clear all headers from a response. | |
| * | |
| * @param {object} res | |
| * @private | |
| */ | |
| function clearHeaders (res) { | |
| for (const header of res.getHeaderNames()) { | |
| res.removeHeader(header) | |
| } | |
| } | |
| /** | |
| * Collapse all leading slashes into a single slash | |
| * | |
| * @param {string} str | |
| * @private | |
| */ | |
| function collapseLeadingSlashes (str) { | |
| for (var i = 0; i < str.length; i++) { | |
| if (str[i] !== '/') { | |
| break | |
| } | |
| } | |
| return i > 1 | |
| ? '/' + str.substr(i) | |
| : str | |
| } | |
| /** | |
| * Determine if path parts contain a dotfile. | |
| * | |
| * @api private | |
| */ | |
| function containsDotFile (parts) { | |
| for (var i = 0; i < parts.length; i++) { | |
| var part = parts[i] | |
| if (part.length > 1 && part[0] === '.') { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| /** | |
| * Create a Content-Range header. | |
| * | |
| * @param {string} type | |
| * @param {number} size | |
| * @param {array} [range] | |
| */ | |
| function contentRange (type, size, range) { | |
| return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size | |
| } | |
| /** | |
| * Create a minimal HTML document. | |
| * | |
| * @param {string} title | |
| * @param {string} body | |
| * @private | |
| */ | |
| function createHtmlDocument (title, body) { | |
| return '<!DOCTYPE html>\n' + | |
| '<html lang="en">\n' + | |
| '<head>\n' + | |
| '<meta charset="utf-8">\n' + | |
| '<title>' + title + '</title>\n' + | |
| '</head>\n' + | |
| '<body>\n' + | |
| '<pre>' + body + '</pre>\n' + | |
| '</body>\n' + | |
| '</html>\n' | |
| } | |
| /** | |
| * Create a HttpError object from simple arguments. | |
| * | |
| * @param {number} status | |
| * @param {Error|object} err | |
| * @private | |
| */ | |
| function createHttpError (status, err) { | |
| if (!err) { | |
| return createError(status) | |
| } | |
| return err instanceof Error | |
| ? createError(status, err, { expose: false }) | |
| : createError(status, err) | |
| } | |
| /** | |
| * decodeURIComponent. | |
| * | |
| * Allows V8 to only deoptimize this fn instead of all | |
| * of send(). | |
| * | |
| * @param {String} path | |
| * @api private | |
| */ | |
| function decode (path) { | |
| try { | |
| return decodeURIComponent(path) | |
| } catch (err) { | |
| return -1 | |
| } | |
| } | |
| /** | |
| * Determine if emitter has listeners of a given type. | |
| * | |
| * The way to do this check is done three different ways in Node.js >= 0.10 | |
| * so this consolidates them into a minimal set using instance methods. | |
| * | |
| * @param {EventEmitter} emitter | |
| * @param {string} type | |
| * @returns {boolean} | |
| * @private | |
| */ | |
| function hasListeners (emitter, type) { | |
| var count = typeof emitter.listenerCount !== 'function' | |
| ? emitter.listeners(type).length | |
| : emitter.listenerCount(type) | |
| return count > 0 | |
| } | |
| /** | |
| * Normalize the index option into an array. | |
| * | |
| * @param {boolean|string|array} val | |
| * @param {string} name | |
| * @private | |
| */ | |
| function normalizeList (val, name) { | |
| var list = [].concat(val || []) | |
| for (var i = 0; i < list.length; i++) { | |
| if (typeof list[i] !== 'string') { | |
| throw new TypeError(name + ' must be array of strings or false') | |
| } | |
| } | |
| return list | |
| } | |
| /** | |
| * Parse an HTTP Date into a number. | |
| * | |
| * @param {string} date | |
| * @private | |
| */ | |
| function parseHttpDate (date) { | |
| var timestamp = date && Date.parse(date) | |
| return typeof timestamp === 'number' | |
| ? timestamp | |
| : NaN | |
| } | |
| /** | |
| * Parse a HTTP token list. | |
| * | |
| * @param {string} str | |
| * @private | |
| */ | |
| function parseTokenList (str) { | |
| var end = 0 | |
| var list = [] | |
| var start = 0 | |
| // gather tokens | |
| for (var i = 0, len = str.length; i < len; i++) { | |
| switch (str.charCodeAt(i)) { | |
| case 0x20: /* */ | |
| if (start === end) { | |
| start = end = i + 1 | |
| } | |
| break | |
| case 0x2c: /* , */ | |
| if (start !== end) { | |
| list.push(str.substring(start, end)) | |
| } | |
| start = end = i + 1 | |
| break | |
| default: | |
| end = i + 1 | |
| break | |
| } | |
| } | |
| // final token | |
| if (start !== end) { | |
| list.push(str.substring(start, end)) | |
| } | |
| return list | |
| } | |
| /** | |
| * Set an object of headers on a response. | |
| * | |
| * @param {object} res | |
| * @param {object} headers | |
| * @private | |
| */ | |
| function setHeaders (res, headers) { | |
| var keys = Object.keys(headers) | |
| for (var i = 0; i < keys.length; i++) { | |
| var key = keys[i] | |
| res.setHeader(key, headers[key]) | |
| } | |
| } | |
Xet Storage Details
- Size:
- 20.3 kB
- Xet hash:
- 15d6a71654549039a6aadbe9303d48ca638de9d77e02acd5dfae42bfc3a5b4fa
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.