From 0598a6398940cf3e04d21b7aaf69455492a713fd Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Sun, 31 Jul 2022 16:34:06 +0700 Subject: [PATCH] refactor: serve handlers/middlewares moved shared codes into serveUtils to reduce complexity --- controllers/handlers/ServeStatic.js | 98 ++++--------------- controllers/middlewares/ServeLiveDirectory.js | 23 +---- controllers/middlewares/ServeStaticQuick.js | 92 +++-------------- controllers/utils/serveUtils.js | 72 ++++++++++++++ 4 files changed, 112 insertions(+), 173 deletions(-) diff --git a/controllers/handlers/ServeStatic.js b/controllers/handlers/ServeStatic.js index a7a2819..fdac1d9 100644 --- a/controllers/handlers/ServeStatic.js +++ b/controllers/handlers/ServeStatic.js @@ -20,7 +20,6 @@ const contentDisposition = require('content-disposition') const etag = require('etag') const fs = require('fs') -const parseRange = require('range-parser') const SimpleDataStore = require('./../utils/SimpleDataStore') const errors = require('./../errorsController') const paths = require('./../pathsController') @@ -152,13 +151,6 @@ class ServeStatic { return stat } - /* - * Based on https://github.com/pillarjs/send/blob/0.18.0/index.js - * Copyright(c) 2012 TJ Holowaychuk - * Copyright(c) 2014-2022 Douglas Christopher Wilson - * MIT Licensed - */ - async #handler (req, res) { if (this.#options.ignorePatterns && this.#options.ignorePatterns.some(pattern => req.path.startsWith(pattern))) { return errors.handleNotFound(req, res) @@ -176,90 +168,38 @@ class ServeStatic { return errors.handleNotFound(req, res) } - // ReadStream options - let len = stat.size - const opts = {} - let ranges = req.headers.range - let offset = 0 - - // set content-type + // Set Content-Type res.type(req.path) - // set header fields + // Set header fields await this.#setHeaders(req, res, stat) - // conditional GET support - if (serveUtils.isConditionalGET(req)) { - if (serveUtils.isPreconditionFailure(req, res)) { - return res.status(412).end() - } - - if (serveUtils.isFresh(req, res)) { - return res.status(304).end() - } + // Conditional GET support + if (serveUtils.assertConditionalGET(req, res)) { + return res.end() } - // adjust len to start/end options - len = Math.max(0, len - offset) - if (opts.end !== undefined) { - const bytes = opts.end - offset + 1 - if (len > bytes) len = bytes - } - - // Range support - if (this.#options.acceptRanges && serveUtils.BYTES_RANGE_REGEXP.test(ranges)) { - // parse - ranges = parseRange(len, ranges, { - combine: true - }) - - // If-Range support - if (!serveUtils.isRangeFresh(req, res)) { - // range stale - ranges = -2 - } - - // unsatisfiable - if (ranges === -1) { - // Content-Range - res.header('Content-Range', serveUtils.contentRange('bytes', len)) - - // 416 Requested Range Not Satisfiable - return res.status(416).end() - } - - // valid (syntactically invalid/multiple ranges are treated as a regular response) - if (ranges !== -2 && ranges.length === 1) { - // Content-Range - res.status(206) - res.header('Content-Range', serveUtils.contentRange('bytes', len, ranges[0])) - - // adjust for requested range - offset += ranges[0].start - len = ranges[0].end - ranges[0].start + 1 - } - } else if (req.method === 'GET' && this.setContentDisposition) { - // Only set Content-Disposition on complete GET requests - // Range requests are typically when streaming - await this.setContentDisposition(req, res) - } - - // set read options - opts.start = offset - opts.end = Math.max(offset, offset + len - 1) + // ReadStream options with Content-Range support if required + const { options, length } = serveUtils.buildReadStreamOptions(req, res, stat, this.#options.acceptRanges) // HEAD support if (req.method === 'HEAD') { // If HEAD, also set Content-Length (must be string) - res.header('Content-Length', String(len)) + res.header('Content-Length', String(length)) return res.end() } - if (len === 0) { + // Only set Content-Disposition on initial GET request + // Skip for subsequent requests on non-zero start byte (e.g. streaming) + if (options.start === 0 && this.setContentDisposition) { + await this.setContentDisposition(req, res) + } + + if (length === 0) { res.end() } - return this.#stream(req, res, fullPath, opts, len) + return this.#stream(req, res, fullPath, options, length) } async #setHeaders (req, res, stat) { @@ -289,8 +229,8 @@ class ServeStatic { } } - async #stream (req, res, fullPath, opts, len) { - const readStream = fs.createReadStream(fullPath, opts) + async #stream (req, res, fullPath, options, length) { + const readStream = fs.createReadStream(fullPath, options) readStream.on('error', error => { readStream.destroy() @@ -298,7 +238,7 @@ class ServeStatic { }) // 2nd param will be set as Content-Length header (must be number) - return res.stream(readStream, len) + return res.stream(readStream, length) } get handler () { diff --git a/controllers/middlewares/ServeLiveDirectory.js b/controllers/middlewares/ServeLiveDirectory.js index 9cc149b..91a0851 100644 --- a/controllers/middlewares/ServeLiveDirectory.js +++ b/controllers/middlewares/ServeLiveDirectory.js @@ -62,29 +62,16 @@ class ServeLiveDirectory { this.#options = options } - /* - * Based on https://github.com/pillarjs/send/blob/0.18.0/index.js - * Copyright(c) 2012 TJ Holowaychuk - * Copyright(c) 2014-2022 Douglas Christopher Wilson - * MIT Licensed - */ - handler (req, res, file) { - // set content-type + // Set Content-Type res.type(file.extension) - // set header fields + // Set header fields this.#setHeaders(req, res, file) - // conditional GET support - if (serveUtils.isConditionalGET(req)) { - if (serveUtils.isPreconditionFailure(req, res)) { - return res.status(412).end() - } - - if (serveUtils.isFresh(req, res)) { - return res.status(304).end() - } + // Conditional GET support + if (serveUtils.assertConditionalGET(req, res)) { + return res.end() } // HEAD support diff --git a/controllers/middlewares/ServeStaticQuick.js b/controllers/middlewares/ServeStaticQuick.js index bc42759..befbde0 100644 --- a/controllers/middlewares/ServeStaticQuick.js +++ b/controllers/middlewares/ServeStaticQuick.js @@ -22,7 +22,6 @@ const chokidar = require('chokidar') const etag = require('etag') const fs = require('fs') -const parseRange = require('range-parser') const serveUtils = require('./../utils/serveUtils') const logger = require('./../../logger') @@ -78,94 +77,33 @@ class ServeStaticQuick { this.#options = options } - /* - * Based on https://github.com/pillarjs/send/blob/0.18.0/index.js - * Copyright(c) 2012 TJ Holowaychuk - * Copyright(c) 2014-2022 Douglas Christopher Wilson - * MIT Licensed - */ - handler (req, res, stat) { - // ReadStream options - let len = stat.size - const opts = {} - let ranges = req.headers.range - let offset = 0 - - // set content-type + // Set Content-Type res.type(req.path) - // set header fields + // Set header fields this.#setHeaders(req, res, stat) - // conditional GET support - if (serveUtils.isConditionalGET(req)) { - if (serveUtils.isPreconditionFailure(req, res)) { - return res.status(412).end() - } - - if (serveUtils.isFresh(req, res)) { - return res.status(304).end() - } + // Conditional GET support + if (serveUtils.assertConditionalGET(req, res)) { + return res.end() } - // adjust len to start/end options - len = Math.max(0, len - offset) - if (opts.end !== undefined) { - const bytes = opts.end - offset + 1 - if (len > bytes) len = bytes - } - - // Range support - if (this.#options.acceptRanges && serveUtils.BYTES_RANGE_REGEXP.test(ranges)) { - // parse - ranges = parseRange(len, ranges, { - combine: true - }) - - // If-Range support - if (!serveUtils.isRangeFresh(req, res)) { - // range stale - ranges = -2 - } - - // unsatisfiable - if (ranges === -1) { - // Content-Range - res.header('Content-Range', serveUtils.contentRange('bytes', len)) - - // 416 Requested Range Not Satisfiable - return res.status(416).end() - } - - // valid (syntactically invalid/multiple ranges are treated as a regular response) - if (ranges !== -2 && ranges.length === 1) { - // Content-Range - res.status(206) - res.header('Content-Range', serveUtils.contentRange('bytes', len, ranges[0])) - - // adjust for requested range - offset += ranges[0].start - len = ranges[0].end - ranges[0].start + 1 - } - } - - // set read options - opts.start = offset - opts.end = Math.max(offset, offset + len - 1) + // ReadStream options with Content-Range support if required + const { options, length } = serveUtils.buildReadStreamOptions(req, res, stat, this.#options.acceptRanges) // HEAD support if (req.method === 'HEAD') { // If HEAD, also set Content-Length (must be string) - res.header('Content-Length', String(len)) + res.header('Content-Length', String(length)) return res.end() } - if (len === 0) { + if (length === 0) { res.end() } - return this.#stream(req, res, stat, opts, len) + return this.#stream(req, res, stat, options, length) } // Returns a promise which resolves to true once ServeStaticQuick is ready @@ -174,7 +112,9 @@ class ServeStaticQuick { if (this.#readyPromise === true) return Promise.resolve(true) // Create a promise if one does not exist for ready event - if (this.#readyPromise === undefined) { this.#readyPromise = new Promise((resolve) => (this.#readyResolve = resolve)) } + if (this.#readyPromise === undefined) { + this.#readyPromise = new Promise((resolve) => (this.#readyResolve = resolve)) + } return this.#readyPromise } @@ -255,9 +195,9 @@ class ServeStaticQuick { } } - #stream (req, res, stat, opts, len) { + #stream (req, res, stat, options, length) { const fullPath = this.directory + req.path - const readStream = fs.createReadStream(fullPath, opts) + const readStream = fs.createReadStream(fullPath, options) readStream.on('error', error => { readStream.destroy() @@ -265,7 +205,7 @@ class ServeStaticQuick { }) // 2nd param will be set as Content-Length header (must be number) - return res.stream(readStream, len) + return res.stream(readStream, length) } get middleware () { diff --git a/controllers/utils/serveUtils.js b/controllers/utils/serveUtils.js index 1d1c134..0625ef7 100644 --- a/controllers/utils/serveUtils.js +++ b/controllers/utils/serveUtils.js @@ -1,4 +1,5 @@ const fresh = require('fresh') +const parseRange = require('range-parser') const self = { BYTES_RANGE_REGEXP: /^ *bytes=/ @@ -116,4 +117,75 @@ self.parseTokenList = str => { return list } +self.assertConditionalGET = (req, res) => { + if (self.isConditionalGET(req)) { + if (self.isPreconditionFailure(req, res)) { + res.status(412) + return true + } + + if (self.isFresh(req, res)) { + res.status(304) + return true + } + } +} + +self.buildReadStreamOptions = (req, res, stat, acceptRanges) => { + // ReadStream options + let length = stat.size + const options = {} + let ranges = req.headers.range + let offset = 0 + + // Adjust len to start/end options + length = Math.max(0, length - offset) + if (options.end !== undefined) { + const bytes = options.end - offset + 1 + if (length > bytes) { + length = bytes + } + } + + // Range support + if (acceptRanges && self.BYTES_RANGE_REGEXP.test(ranges)) { + // Parse + ranges = parseRange(length, ranges, { + combine: true + }) + + // If-Range support + if (!self.isRangeFresh(req, res)) { + // Stale + ranges = -2 + } + + // Unsatisfiable + if (ranges === -1) { + // Content-Range + res.header('Content-Range', self.contentRange('bytes', length)) + + // 416 Requested Range Not Satisfiable + return res.status(416).end() + } + + // Valid (syntactically invalid/multiple ranges are treated as a regular response) + if (ranges !== -2 && ranges.length === 1) { + // Content-Range + res.status(206) + res.header('Content-Range', self.contentRange('bytes', length, ranges[0])) + + // Adjust for requested range + offset += ranges[0].start + length = ranges[0].end - ranges[0].start + 1 + } + } + + // Set read options + options.start = offset + options.end = Math.max(offset, offset + length - 1) + + return { options, length } +} + module.exports = self