orator-static-server 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,51 +1,97 @@
1
- const FableServiceProviderBase = require('fable-serviceproviderbase');
1
+ /**
2
+ * Orator Static Server
3
+ *
4
+ * Static file serving for Orator API servers. Handles MIME type detection,
5
+ * route stripping, default files, magic subdomain folder mapping, and
6
+ * Content-Type headers so browsers render HTML instead of downloading it.
7
+ *
8
+ * @license MIT
9
+ *
10
+ * @author Steven Velozo <steven@velozo.com>
11
+ */
12
+
13
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
14
 
3
15
  const libServeStatic = require('serve-static');
4
16
  const libFinalHandler = require('finalhandler');
17
+ const libMime = require('mime');
5
18
 
6
19
  /**
7
- * Fable service that provides a simple static file server.
20
+ * @class OratorStaticServer
21
+ * @extends libFableServiceProviderBase
22
+ *
23
+ * A service provider that manages static file serving routes on an Orator instance.
8
24
  */
9
- class OratorStaticFileService extends FableServiceProviderBase
25
+ class OratorStaticServer extends libFableServiceProviderBase
10
26
  {
11
- /**
12
- * Construct a service instance.
13
- *
14
- * @param {object} pFable The fable instance for the application. Used for logging and settings.
15
- * @param {object} pOptions Custom settings for this service instance.
16
- * @param {string} pServiceHash The hash for this service instance.
17
- *
18
- * @return a static file service instance.
19
- */
20
27
  constructor(pFable, pOptions, pServiceHash)
21
28
  {
22
29
  super(pFable, pOptions, pServiceHash);
23
30
 
24
- if (typeof(this.options.proxyUrl) != 'string' || !this.options.proxyUrl.startsWith('http'))
31
+ this.serviceType = 'OratorStaticServer';
32
+
33
+ // Keep track of registered static routes for introspection
34
+ this.routes = [];
35
+
36
+ // This is here because libMime has a breaking change from v1 to v2 and the lookup function was update to be getType per https://stackoverflow.com/a/60741078
37
+ // We don't want to introspect properties on this library every single time we need to check mime types.
38
+ // Therefore we are setting this boolean here and using it to branch.
39
+ this.oldLibMime = false;
40
+ if ('lookup' in libMime)
25
41
  {
26
- this.log.trace('API proxy url falling back to settings...', { badUrl: this.options.proxyUrl });
27
- this.options.proxyUrl = this.fable.settings.APIProxyUrl;
42
+ this.oldLibMime = true;
28
43
  }
29
44
  }
30
45
 
46
+ /**
47
+ * Set the Content-Type header on a response based on the file name.
48
+ *
49
+ * @param {string} pFileName - The file name (or path) to detect MIME type from.
50
+ * @param {Object} pResponse - The HTTP response object.
51
+ */
52
+ setMimeHeader(pFileName, pResponse)
53
+ {
54
+ let tmpHeader;
55
+
56
+ if (this.oldLibMime)
57
+ {
58
+ tmpHeader = libMime.lookup(pFileName);
59
+ }
60
+ else
61
+ {
62
+ tmpHeader = libMime.getType(pFileName);
63
+ }
64
+
65
+ if (!tmpHeader)
66
+ {
67
+ tmpHeader = 'application/octet-stream';
68
+ }
69
+
70
+ pResponse.setHeader('Content-Type', tmpHeader);
71
+ }
31
72
 
32
73
  /**
33
- * Brought over from old orator and ported to work in the same way.
74
+ * Add a static file serving route to the Orator instance.
34
75
  *
35
- * @param {object} pOrator The Orator instance.
36
- * @param {string} pFilePath The path on disk that we are serving files from.
37
- * @param {string?} pDefaultFile (optional) The default file served if no specific file is requested.
38
- * @param {string?} pRoute (optional) The route matcher that will be used. Defaults to everything.
39
- * @param {string?} pRouteStrip (optional) If provided, this prefix will be removed from URL paths before being served.
40
- * @param {object?} pParams (optional) Additional parameters to pass to serve-static.
41
- * @return {boolean} true if the handler was successfully installed, otherwise false.
76
+ * @param {string} pFilePath - The path on disk to serve files from.
77
+ * @param {string} [pDefaultFile='index.html'] - The default file for directory requests.
78
+ * @param {string} [pRoute='/*'] - The route pattern to match.
79
+ * @param {string} [pRouteStrip='/'] - URL prefix to strip before filesystem lookup.
80
+ * @param {object} [pParams={}] - Additional parameters passed to serve-static.
81
+ * @returns {boolean} true if the route was successfully installed.
42
82
  */
43
- addStaticRoute(pOrator, pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams)
83
+ addStaticRoute(pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams)
44
84
  {
85
+ if (!this.fable.Orator)
86
+ {
87
+ this.log.error('OratorStaticServer requires an Orator instance to be registered with Fable.');
88
+ return false;
89
+ }
90
+
45
91
  if (typeof(pFilePath) !== 'string')
46
92
  {
47
- pOrator.fable.log.error('A file path must be passed in as part of the server.');
48
- return false;
93
+ this.fable.log.error('A file path must be passed in as part of the server.');
94
+ return false;
49
95
  }
50
96
 
51
97
  // Default to just serving from root
@@ -55,44 +101,91 @@ class OratorStaticFileService extends FableServiceProviderBase
55
101
  // Default to serving index.html
56
102
  const tmpDefaultFile = (typeof(pDefaultFile) === 'undefined') ? 'index.html' : pDefaultFile;
57
103
 
58
- pOrator.fable.log.info('Orator mapping static route to files: '+tmpRoute+' ==> '+pFilePath+' '+tmpDefaultFile);
104
+ let tmpOrator = this.fable.Orator;
59
105
 
60
- // Add the route
61
- pOrator.serviceServer.server.get(tmpRoute, (pRequest, pResponse, fNext) =>
106
+ this.fable.log.info('Orator mapping static route to files: '+tmpRoute+' ==> '+pFilePath+' '+tmpDefaultFile);
107
+
108
+ // Ensure FilePersistence is available for the magic subdomain subfolder check
109
+ if (!this.fable.FilePersistence)
62
110
  {
63
- // The split removes query string parameters so they are ignored by our static web server.
64
- // The substring cuts that out from the file path so relative files serve from the folders and server
65
- //FIXME: .....
66
- // See if there is a magic subdomain put at the beginning of a request.
67
- // If there is, then we need to see if there is a subfolder and add that to the file path
68
- let tmpHostSet = pRequest.headers.host.split('.');
69
- let tmpPotentialSubfolderMagicHost = false;
70
- let servePath = pFilePath;
71
- // Check if there are more than one host in the host header (this will be 127 a lot)
72
- if (tmpHostSet.length > 1)
73
- {
74
- tmpPotentialSubfolderMagicHost = tmpHostSet[0];
75
- }
76
- if (tmpPotentialSubfolderMagicHost)
77
- {
78
- // Check if the subfolder exists
79
- let tmpPotentialSubfolder = servePath + tmpPotentialSubfolderMagicHost;
80
- if (this.fable.FilePersistence.libFS.existsSync(tmpPotentialSubfolder))
111
+ this.fable.serviceManager.instantiateServiceProvider('FilePersistence');
112
+ }
113
+
114
+ // Try the service server's built-in serveStatic first (e.g. restify's serveStaticFiles plugin).
115
+ // This handles MIME types, caching headers, and streaming correctly without our manual intervention.
116
+ if (typeof(tmpOrator.serviceServer.serveStatic) === 'function')
117
+ {
118
+ let tmpServeStaticOptions = Object.assign({ directory: pFilePath, default: tmpDefaultFile }, pParams);
119
+ if (tmpOrator.serviceServer.serveStatic(tmpRoute, tmpServeStaticOptions))
120
+ {
121
+ this.routes.push(
122
+ {
123
+ filePath: pFilePath,
124
+ defaultFile: tmpDefaultFile,
125
+ route: tmpRoute,
126
+ routeStrip: tmpRouteStrip,
127
+ params: pParams || {}
128
+ });
129
+ return true;
130
+ }
131
+ }
132
+
133
+ // Fall back to the serve-static library approach (used by the IPC service server and other
134
+ // service servers that don't have a built-in serveStatic implementation).
135
+ tmpOrator.serviceServer.get(tmpRoute,
136
+ (pRequest, pResponse, fNext) =>
137
+ {
138
+ // See if there is a magic subdomain put at the beginning of a request.
139
+ // If there is, then we need to see if there is a subfolder and add that to the file path
140
+ let tmpHostSet = pRequest.headers.host.split('.');
141
+ let tmpPotentialSubfolderMagicHost = false;
142
+ let servePath = pFilePath;
143
+ // Check if there are more than one host in the host header (this will be 127 a lot)
144
+ if (tmpHostSet.length > 1)
145
+ {
146
+ tmpPotentialSubfolderMagicHost = tmpHostSet[0];
147
+ }
148
+ if (tmpPotentialSubfolderMagicHost)
149
+ {
150
+ // Check if the subfolder exists -- this is only one dimensional for now
151
+ let tmpPotentialSubfolder = servePath + tmpPotentialSubfolderMagicHost;
152
+ if (this.fable.FilePersistence.libFS.existsSync(tmpPotentialSubfolder))
153
+ {
154
+ // If it does, then we need to add it to the file path
155
+ servePath = `${tmpPotentialSubfolder}/`;
156
+ }
157
+ }
158
+ pRequest.url = pRequest.url.split('?')[0].substr(tmpRouteStrip.length) || '/';
159
+ pRequest.path = function()
160
+ {
161
+ return pRequest.url;
162
+ };
163
+
164
+ // When the URL is a directory (e.g. '/' or '/docs/'), use the default file for MIME detection
165
+ // so the browser gets text/html instead of application/octet-stream
166
+ let tmpMimeTarget = pRequest.url;
167
+ if (tmpMimeTarget.endsWith('/') || tmpMimeTarget.indexOf('.') < 0)
81
168
  {
82
- // If it does, then we need to add it to the file path
83
- servePath = `${tmpPotentialSubfolder}/`;
169
+ tmpMimeTarget = tmpDefaultFile;
84
170
  }
85
- }
86
- pRequest.url = pRequest.url.split('?')[0].substr(tmpRouteStrip.length) || '/';
87
- pRequest.path = function()
88
- {
89
- return pRequest.url;
90
- };
91
- const tmpServe = libServeStatic(servePath, Object.assign({ index: tmpDefaultFile }, pParams));
92
- tmpServe(pRequest, pResponse, libFinalHandler(pRequest, pResponse));
93
- });
171
+ this.setMimeHeader(tmpMimeTarget, pResponse);
172
+
173
+ const tmpServe = libServeStatic(servePath, Object.assign({ index: tmpDefaultFile }, pParams));
174
+ tmpServe(pRequest, pResponse, libFinalHandler(pRequest, pResponse));
175
+ // TODO: This may break things if a post request send handler is setup...
176
+ //fNext();
177
+ });
178
+
179
+ this.routes.push(
180
+ {
181
+ filePath: pFilePath,
182
+ defaultFile: tmpDefaultFile,
183
+ route: tmpRoute,
184
+ routeStrip: tmpRouteStrip,
185
+ params: pParams || {}
186
+ });
94
187
  return true;
95
188
  }
96
189
  }
97
190
 
98
- module.exports = OratorStaticFileService;
191
+ module.exports = OratorStaticServer;