1 |
|
2 |
|
3 |
|
4 | const path = require("path");
|
5 | const fs = require("fs");
|
6 | const express = require("express");
|
7 | const expressStaticGzip = require("express-static-gzip");
|
8 | const compression = require('compression');
|
9 | const nakedRedirect = require('express-naked-redirect');
|
10 | const utils = require("./utils");
|
11 | const version = require('../src/version').version;
|
12 | const chalk = require('chalk');
|
13 | const SUPPRESS = require('argparse').Const.SUPPRESS;
|
14 |
|
15 |
|
16 | const addParser = (parser) => {
|
17 | const description = `Launch a local server to view locally available datasets & narratives.
|
18 | The handlers for (auspice) client requests can be overridden here (see documentation for more details).
|
19 | If you want to serve a customised auspice client then you must have run "auspice build" in the same directory
|
20 | as you run "auspice view" from.
|
21 | `;
|
22 | const subparser = parser.addParser('view', {addHelp: true, description});
|
23 | subparser.addArgument('--verbose', {action: "storeTrue", help: "Print more verbose logging messages."});
|
24 | subparser.addArgument('--handlers', {action: "store", metavar: "JS", help: "Overwrite the provided server handlers for client requests. See documentation for more details."});
|
25 | subparser.addArgument('--datasetDir', {metavar: "PATH", help: "Directory where datasets (JSONs) are sourced. This is ignored if you define custom handlers."});
|
26 | subparser.addArgument('--narrativeDir', {metavar: "PATH", help: "Directory where narratives (Markdown files) are sourced. This is ignored if you define custom handlers."});
|
27 |
|
28 | subparser.addArgument('--customBuild', {action: "storeTrue", help: SUPPRESS});
|
29 | subparser.addArgument('--gh-pages', {action: "store", help: SUPPRESS});
|
30 | };
|
31 |
|
32 | const serveRelativeFilepaths = ({app, dir}) => {
|
33 | app.get("*.json", (req, res) => {
|
34 | const filePath = path.join(dir, req.originalUrl);
|
35 | utils.log(`${req.originalUrl} -> ${filePath}`);
|
36 | res.sendFile(filePath);
|
37 | });
|
38 | return `JSON requests will be served relative to ${dir}.`;
|
39 | };
|
40 |
|
41 | const loadAndAddHandlers = ({app, handlersArg, datasetDir, narrativeDir}) => {
|
42 |
|
43 | const handlers = {};
|
44 | let datasetsPath, narrativesPath;
|
45 | if (handlersArg) {
|
46 | const handlersPath = path.resolve(handlersArg);
|
47 | utils.verbose(`Loading handlers from ${handlersPath}`);
|
48 | const inject = require(handlersPath);
|
49 | handlers.getAvailable = inject.getAvailable;
|
50 | handlers.getDataset = inject.getDataset;
|
51 | handlers.getNarrative = inject.getNarrative;
|
52 | } else {
|
53 | datasetsPath = utils.resolveLocalDirectory(datasetDir, false);
|
54 | narrativesPath = utils.resolveLocalDirectory(narrativeDir, true);
|
55 | handlers.getAvailable = require("./server/getAvailable")
|
56 | .setUpGetAvailableHandler({datasetsPath, narrativesPath});
|
57 | handlers.getDataset = require("./server/getDataset")
|
58 | .setUpGetDatasetHandler({datasetsPath});
|
59 | handlers.getNarrative = require("./server/getNarrative")
|
60 | .setUpGetNarrativeHandler({narrativesPath});
|
61 | }
|
62 |
|
63 |
|
64 | app.get("/charon/getAvailable", handlers.getAvailable);
|
65 | app.get("/charon/getDataset", handlers.getDataset);
|
66 | app.get("/charon/getNarrative", handlers.getNarrative);
|
67 | app.get("/charon*", (req, res) => {
|
68 | res.statusMessage = "Query unhandled -- " + req.originalUrl;
|
69 | utils.warn(res.statusMessage);
|
70 | return res.status(500).end();
|
71 | });
|
72 |
|
73 | return handlersArg ?
|
74 | `Custom server handlers provided.` :
|
75 | `Looking for datasets in ${datasetsPath}\nLooking for narratives in ${narrativesPath}`;
|
76 | };
|
77 |
|
78 | const getAuspiceBuild = () => {
|
79 | const cwd = path.resolve(process.cwd());
|
80 | const sourceDir = path.resolve(__dirname, "..");
|
81 | if (
|
82 | cwd !== sourceDir &&
|
83 | fs.existsSync(path.join(cwd, "index.html")) &&
|
84 | fs.existsSync(path.join(cwd, "dist")) &&
|
85 | fs.existsSync(path.join(cwd, "dist", "auspice.bundle.js"))
|
86 | ) {
|
87 | return {
|
88 | message: "Serving the auspice build which exists in this directory.",
|
89 | baseDir: cwd,
|
90 | distDir: path.join(cwd, "dist")
|
91 | };
|
92 | }
|
93 | return {
|
94 | message: `Serving auspice version ${version}`,
|
95 | baseDir: sourceDir,
|
96 | distDir: path.join(sourceDir, "dist")
|
97 | };
|
98 | };
|
99 |
|
100 | const run = (args) => {
|
101 |
|
102 | const app = express();
|
103 | app.set('port', process.env.PORT || 4000);
|
104 | app.set('host', process.env.HOST || "localhost");
|
105 | app.use(compression());
|
106 | app.use(nakedRedirect({reverse: true}));
|
107 |
|
108 | if (args.customBuild) {
|
109 | 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");
|
110 | }
|
111 |
|
112 | const auspiceBuild = getAuspiceBuild();
|
113 | utils.verbose(`Serving index / favicon etc from "${auspiceBuild.baseDir}"`);
|
114 | utils.verbose(`Serving built javascript from "${auspiceBuild.distDir}"`);
|
115 | app.get("/favicon.png", (req, res) => {res.sendFile(path.join(auspiceBuild.baseDir, "favicon.png"));});
|
116 | app.use("/dist", expressStaticGzip(auspiceBuild.distDir));
|
117 | app.use(express.static(auspiceBuild.distDir));
|
118 |
|
119 | let handlerMsg = "";
|
120 | if (args.gh_pages) {
|
121 | handlerMsg = serveRelativeFilepaths({app, dir: path.resolve(args.gh_pages)});
|
122 | } else {
|
123 | handlerMsg = loadAndAddHandlers({app, handlersArg: args.handlers, datasetDir: args.datasetDir, narrativeDir: args.narrativeDir});
|
124 | }
|
125 |
|
126 |
|
127 | app.get("*", (req, res) => {
|
128 | res.sendFile(path.join(auspiceBuild.baseDir, "index.html"));
|
129 | });
|
130 |
|
131 | const server = app.listen(app.get('port'), app.get('host'), () => {
|
132 | utils.log("\n\n---------------------------------------------------");
|
133 | const host = app.get('host');
|
134 | const {port} = server.address();
|
135 | console.log(chalk.blueBright("Auspice server now running at ") + chalk.blueBright.underline.bold(`http://${host}:${port}`));
|
136 | utils.log(auspiceBuild.message);
|
137 | utils.log(handlerMsg);
|
138 | utils.log("---------------------------------------------------\n\n");
|
139 | }).on('error', (err) => {
|
140 | if (err.code === 'EADDRINUSE') {
|
141 | utils.error(`Port ${app.get('port')} is currently in use by another program.
|
142 | You must either close that program or specify a different port by setting the shell variable
|
143 | "$PORT". Note that on MacOS / Linux, "lsof -n -i :${app.get('port')} | grep LISTEN" should
|
144 | identify the process currently using the port.`);
|
145 | }
|
146 | utils.error(`Uncaught error in app.listen(). Code: ${err.code}`);
|
147 | });
|
148 |
|
149 | };
|
150 |
|
151 | module.exports = {
|
152 | addParser,
|
153 | run,
|
154 | loadAndAddHandlers,
|
155 | serveRelativeFilepaths
|
156 | };
|