auspice
Version:
Web app for visualizing pathogen evolution
157 lines (140 loc) • 6.85 kB
JavaScript
/* eslint no-console: off */
/* eslint global-require: off */
const path = require("path");
const fs = require("fs");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
const compression = require('compression');
const nakedRedirect = require('express-naked-redirect');
const utils = require("./utils");
const version = require('../src/version').version;
const chalk = require('chalk');
const SUPPRESS = require('argparse').Const.SUPPRESS;
const addParser = (parser) => {
const description = `Launch a local server to view locally available datasets & narratives.
The handlers for (auspice) client requests can be overridden here (see documentation for more details).
If you want to serve a customised auspice client then you must have run "auspice build" in the same directory
as you run "auspice view" from.
`;
const subparser = parser.addParser('view', {addHelp: true, description});
subparser.addArgument('--verbose', {action: "storeTrue", help: "Print more verbose logging messages."});
subparser.addArgument('--handlers', {action: "store", metavar: "JS", help: "Overwrite the provided server handlers for client requests. See documentation for more details."});
subparser.addArgument('--datasetDir', {metavar: "PATH", help: "Directory where datasets (JSONs) are sourced. This is ignored if you define custom handlers."});
subparser.addArgument('--narrativeDir', {metavar: "PATH", help: "Directory where narratives (Markdown files) are sourced. This is ignored if you define custom handlers."});
/* there are some options which we deliberately do not document via `--help`. */
subparser.addArgument('--customBuild', {action: "storeTrue", help: SUPPRESS}); /* see printed warning in the code below */
subparser.addArgument('--gh-pages', {action: "store", help: SUPPRESS}); /* related to the "static-site-generation" or "github-pages" */
};
const serveRelativeFilepaths = ({app, dir}) => {
app.get("*.json", (req, res) => {
const filePath = path.join(dir, req.originalUrl);
utils.log(`${req.originalUrl} -> ${filePath}`);
res.sendFile(filePath);
});
return `JSON requests will be served relative to ${dir}.`;
};
const loadAndAddHandlers = ({app, handlersArg, datasetDir, narrativeDir}) => {
/* load server handlers, either from provided path or the defaults */
const handlers = {};
let datasetsPath, narrativesPath;
if (handlersArg) {
const handlersPath = path.resolve(handlersArg);
utils.verbose(`Loading handlers from ${handlersPath}`);
const inject = require(handlersPath); // eslint-disable-line
handlers.getAvailable = inject.getAvailable;
handlers.getDataset = inject.getDataset;
handlers.getNarrative = inject.getNarrative;
} else {
datasetsPath = utils.resolveLocalDirectory(datasetDir, false);
narrativesPath = utils.resolveLocalDirectory(narrativeDir, true);
handlers.getAvailable = require("./server/getAvailable")
.setUpGetAvailableHandler({datasetsPath, narrativesPath});
handlers.getDataset = require("./server/getDataset")
.setUpGetDatasetHandler({datasetsPath});
handlers.getNarrative = require("./server/getNarrative")
.setUpGetNarrativeHandler({narrativesPath});
}
/* apply handlers */
app.get("/charon/getAvailable", handlers.getAvailable);
app.get("/charon/getDataset", handlers.getDataset);
app.get("/charon/getNarrative", handlers.getNarrative);
app.get("/charon*", (req, res) => {
res.statusMessage = "Query unhandled -- " + req.originalUrl;
utils.warn(res.statusMessage);
return res.status(500).end();
});
return handlersArg ?
`Custom server handlers provided.` :
`Looking for datasets in ${datasetsPath}\nLooking for narratives in ${narrativesPath}`;
};
const getAuspiceBuild = () => {
const cwd = path.resolve(process.cwd());
const sourceDir = path.resolve(__dirname, "..");
if (
cwd !== sourceDir &&
fs.existsSync(path.join(cwd, "index.html")) &&
fs.existsSync(path.join(cwd, "dist")) &&
fs.existsSync(path.join(cwd, "dist", "auspice.bundle.js"))
) {
return {
message: "Serving the auspice build which exists in this directory.",
baseDir: cwd,
distDir: path.join(cwd, "dist")
};
}
return {
message: `Serving auspice version ${version}`,
baseDir: sourceDir,
distDir: path.join(sourceDir, "dist")
};
};
const run = (args) => {
/* Basic server set up */
const app = express();
app.set('port', process.env.PORT || 4000);
app.set('host', process.env.HOST || "localhost");
app.use(compression());
app.use(nakedRedirect({reverse: true})); /* redirect www.name.org to name.org */
if (args.customBuild) {
utils.warn("--customBuild is no longer used and will be removed in a future version. We now serve a custom auspice build if one exists in the directory `auspice view` is run from");
}
const auspiceBuild = getAuspiceBuild();
utils.verbose(`Serving index / favicon etc from "${auspiceBuild.baseDir}"`);
utils.verbose(`Serving built javascript from "${auspiceBuild.distDir}"`);
app.get("/favicon.png", (req, res) => {res.sendFile(path.join(auspiceBuild.baseDir, "favicon.png"));});
app.use("/dist", expressStaticGzip(auspiceBuild.distDir));
app.use(express.static(auspiceBuild.distDir));
let handlerMsg = "";
if (args.gh_pages) {
handlerMsg = serveRelativeFilepaths({app, dir: path.resolve(args.gh_pages)});
} else {
handlerMsg = loadAndAddHandlers({app, handlersArg: args.handlers, datasetDir: args.datasetDir, narrativeDir: args.narrativeDir});
}
/* this must be the last "get" handler, else the "*" swallows all other requests */
app.get("*", (req, res) => {
res.sendFile(path.join(auspiceBuild.baseDir, "index.html"));
});
const server = app.listen(app.get('port'), app.get('host'), () => {
utils.log("\n\n---------------------------------------------------");
const host = app.get('host');
const {port} = server.address();
console.log(chalk.blueBright("Auspice server now running at ") + chalk.blueBright.underline.bold(`http://${host}:${port}`));
utils.log(auspiceBuild.message);
utils.log(handlerMsg);
utils.log("---------------------------------------------------\n\n");
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
utils.error(`Port ${app.get('port')} is currently in use by another program.
You must either close that program or specify a different port by setting the shell variable
"$PORT". Note that on MacOS / Linux, "lsof -n -i :${app.get('port')} | grep LISTEN" should
identify the process currently using the port.`);
}
utils.error(`Uncaught error in app.listen(). Code: ${err.code}`);
});
};
module.exports = {
addParser,
run,
loadAndAddHandlers,
serveRelativeFilepaths
};