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
JavaScript
// 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/>.
*/
;
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;