UNPKG

superstatic

Version:

A static file server for fancy apps

193 lines (192 loc) 6.61 kB
"use strict"; /** * Copyright (c) 2022 Google LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ const _ = require("lodash"); const mime = require("mime-types"); const path = require("path"); const onFinished = require("on-finished"); const destroy = require("destroy"); const awaitFinished = (res) => { return new Promise((resolve) => { onFinished(res, resolve); }); }; /** @type Class */ const Responder = function (req, res, options) { this.req = req; this.res = res; this.provider = options.provider; this.config = options.config || {}; this.rewriters = options.rewriters || {}; this.compressor = options.compressor; }; Responder.prototype.isNotModified = function (stats) { if (stats.etag && stats.etag === this.req.headers["if-none-match"]) { return true; } let reqModified = this.req.headers["if-modified-since"]; if (reqModified) { reqModified = new Date(reqModified).getTime(); } if (stats.modified && reqModified && stats.modified < reqModified) { return true; } return false; }; Responder.prototype.handle = function (item, next) { const self = this; return this._handle(item) .then((responded) => { if (!responded && next) { next(); } return responded; }) .catch((err) => { return self.handleError(err); }); }; Responder.prototype._handle = function (item) { if (_.isArray(item)) { return this.handleStack(item); } else if (_.isString(item)) { return this.handleFile({ file: item }); } else if (_.isPlainObject(item)) { if (item.file) { return this.handleFile(item); } else if (item.redirect) { return this.handleRedirect(item); } else if (item.rewrite) { return this.handleRewrite(item); } else if (item.data) { return this.handleData(item); } } else if (_.isFunction(item)) { return this.handleMiddleware(item); } return Promise.reject(new Error(JSON.stringify(item) + " is not a recognized responder directive")); }; Responder.prototype.handleError = function (err) { this.res.statusCode = 500; console.log(err.stack); this.res.end("Unexpected error occurred."); }; Responder.prototype.handleStack = function (stack) { const self = this; if (stack.length) { return this._handle(stack.shift()).then((responded) => { return responded ? true : self.handleStack(stack); }); } return Promise.resolve(false); }; Responder.prototype.handleFile = function (file) { const self = this; return this.provider(this.req, file.file).then((result) => { if (!result) { return false; } if (self.isNotModified(result)) { return self.handleNotModified(result); } return self.handleFileStream(file, result); }); }; Responder.prototype.handleFileStream = function (file, result) { const self = this; this.streamedFile = file; this.res.statusCode = file.status || 200; if (this.res.statusCode === 200 && file.file === this.config.errorPage) { this.res.statusCode = 404; } this.res.setHeader("Content-Type", result.contentType || mime.contentType(path.extname(file.file))); if (result.size) { this.res.setHeader("Content-Length", result.size); } if (result.etag) { this.res.setHeader("ETag", result.etag); } if (result.modified) { this.res.setHeader("Last-Modified", new Date(result.modified).toUTCString()); } if (this.compressor) { this.compressor(this.req, this.res, () => { result.stream.pipe(self.res); }); } else { result.stream.pipe(self.res); } return awaitFinished(this.res).then(() => { destroy(result.stream); return true; }); }; Responder.prototype.handleNotModified = function () { this.res.statusCode = 304; this.res.removeHeader("Content-Type"); this.res.removeHeader("Content-Length"); this.res.removeHeader("Transfer-Encoding"); this.res.end(); return true; }; Responder.prototype.handleRedirect = function (redirect) { this.res.statusCode = redirect.status || 301; this.res.setHeader("Location", redirect.redirect); this.res.setHeader("Content-Type", "text/html; charset=utf-8"); this.res.end("Redirecting to " + redirect.redirect); return Promise.resolve(true); }; Responder.prototype.handleMiddleware = function (middleware) { const self = this; return new Promise((resolve) => { middleware(self.req, self.res, () => { resolve(false); }); }); }; Responder.prototype.handleRewrite = function (item) { const self = this; if (item.rewrite.destination) { return self.handleFile({ file: item.rewrite.destination }); } for (const key in this.rewriters) { if (item.rewrite[key]) { return this.rewriters[key](item.rewrite, this).then((result) => { return self._handle(result); }); } } return Promise.reject(new Error("Unable to find a matching rewriter for " + JSON.stringify(item.rewrite))); }; Responder.prototype.handleData = function (data) { this.res.statusCode = data.status || 200; this.res.setHeader("Content-Type", data.contentType || "text/html; charset=utf-8"); this.res.end(data.data); return Promise.resolve(true); }; module.exports = Responder;