UNPKG

autostrada

Version:

Auto routes the content of a www directory. You can add a filter to have custom URLs.

230 lines (199 loc) 5.93 kB
// Copyright 2019 Clément Saccoccio /* This file is part of Autostrada. Autostrada is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. Autostrada is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Autostrada. If not, see <https://www.gnu.org/licenses/>. */ 'use strict'; const fs = require('fs'); const path = require('path'); const escapeStringRegexp = require('escape-string-regexp'); const isDirectory = require('is-directory'); const chokidar = require('chokidar'); let root, indexableFilesFilter, fromPathToUrl; let routes = []; let reqWaitingForFSEvent = []; let middelwareDeprecationMsgShown = false const regexpSafePathSep = escapeStringRegexp(path.sep); const deprecationMsg = '' + 'old way to do it:\n' + ' const autostrada = require(\'autostrada\');\n' + ' autostrada.init(options);\n' + ' app.use(autostrada.middleware);\n' + 'new way to do it:\n' + ' const autostrada = require(\'autostrada\')(options)\n' + ' app.use(autostrada);'; function toPosixFilePath(filePath) { return filePath.replace(new RegExp(regexpSafePathSep, 'g'), '/'); } function getMainModuleDirName() { return path.dirname(require.main.filename); } function closeTicket(ticket) { clearTimeout(ticket.timeoutId); const i = reqWaitingForFSEvent.indexOf(ticket); if (~i) reqWaitingForFSEvent.splice(i, 1); } function normalizeOptions(options) { if (typeof options.wwwDirectory !== 'string') { options.wwwDirectory = `${getMainModuleDirName()}${path.sep}www${path.sep}`; } else if (!options.wwwDirectory.endsWith(path.sep)) { options.wwwDirectory += path.sep; } if (!isDirectory.sync(options.wwwDirectory)) { throw `'${options.wwwDirectory}' is not a directory` } if (typeof options.indexableFilesFilter !== 'function') { options.indexableFilesFilter = () => true; } else if (path.sep !== '/') { const indexableFilesFilter = options.indexableFilesFilter; options.indexableFilesFilter = filePath => indexableFilesFilter(toPosixFilePath(filePath)); } if (typeof options.fromPathToUrl !== 'function') { if (path.sep === '/') { options.fromPathToUrl = filePath => filePath; } else { options.fromPathToUrl = toPosixFilePath; } } else if (path.sep !== '/') { const fromPathToUrl = options.fromPathToUrl; options.fromPathToUrl = filePath => fromPathToUrl(toPosixFilePath(filePath)); } } function addFile(filePath) { let url = filePath.substring(root.length - 1); if (indexableFilesFilter(url)) { url = fromPathToUrl(url); const route = { filePath, url, }; routes.push(route); for (const ticket of reqWaitingForFSEvent) { if (typeof url === 'string') { if (url === ticket.url) { sendFile(route, ticket.res, null, () => closeTicket(ticket)); } } else { const i = route.url.indexOf(url); if (~i) { if (i === 0) { sendFile(route, ticket.res, null, () => closeTicket(ticket)); } else { redirect(route, ticket.res, null, () => closeTicket(ticket)); } } } } } } function removeFile(filePath) { for (let i = 0; i < routes.length; i++) { if (routes[i].filePath === filePath) { routes.splice(i, 1); break; } } } function init(options) { if (typeof options !== 'object') options = {}; normalizeOptions(options); root = options.wwwDirectory; indexableFilesFilter = options.indexableFilesFilter; fromPathToUrl = options.fromPathToUrl; chokidar.watch(root).on('add', addFile); chokidar.watch(root).on('unlink', removeFile); } /** * checks if file exists: * - yes -> triggers 'responseSender' * - no -> deletes route and triggers 'next' */ function sendResponse(route, next, responseSender) { fs.access(route.filePath, fs.constants.R_OK, (err) => { if (err) { let i = routes.indexOf(route); if (~i) routes.splice(i, 1); if (next) next(); } else responseSender(); }); } function sendFile(route, res, next, onSuccess) { sendResponse(route, next, () => { res.status(200).sendFile(route.filePath); if (onSuccess) onSuccess(); }); } function redirect(route, res, next, onSuccess) { sendResponse(route, next, () => { res.status(302).redirect(route.url[0]); if (onSuccess) onSuccess(); }); } function protoMiddleware(url, res, next) { for (const route of routes) { if (typeof route.url === 'string') { if (url === route.url) { sendFile(route, res, next); return; } } else { const index = route.url.indexOf(url); if (~index) { if (index === 0) { sendFile(route, res, next); } else { redirect(route, res, next); } return; } } } next(); } function middleware(req, res, next) { protoMiddleware(decodeURIComponent(req.path), res, next); } middleware.lastChance = timeout => { return (req, res, next) => { const url = decodeURIComponent(req.path); protoMiddleware(url, res, () => { const ticket = { url, res, timeoutId: setTimeout(() => { closeTicket(ticket); next(); }, timeout), }; reqWaitingForFSEvent.push(ticket); }); } } function autostrada(options) { init(options); return middleware; } autostrada.init = options => { console.warn('autostrada.init is deprecated since 1.2.0'); console.warn(deprecationMsg); init(options); }; autostrada.middleware = (req, res, next) => { if (!middelwareDeprecationMsgShown) { middelwareDeprecationMsgShown = true; console.warn('autostrada.middleware is deprecated since 1.2.0'); console.warn(deprecationMsg); } middleware(req, res, next); }; autostrada.default = autostrada; module.exports = autostrada;