allow additional arguments to be passed to handler functions
[squeep-api-dingus] / lib / dingus.js
index c8e4909bf1edca17202e1ca26a3310b9d748ffb3..343283974d7c723a6cbb8eb85fc3e3b942230fff 100644 (file)
@@ -26,6 +26,7 @@ const defaultOptions = {
   proxyPrefix: '',
   strictAccept: true,
   selfBaseUrl: '',
+  staticMetadata: true,
   trustProxy: true,
   querystring,
 };
@@ -38,6 +39,7 @@ class Dingus {
    * @param {string} options.proxyPrefix leading part of url path to strip
    * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
    * @param {string} options.selfBaseUrl for constructing links
+   * @param {Boolean} options.staticMetadata serve static headers with static files
    * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
    * @param {Object} options.querystring alternate qs parser to use
    */
@@ -103,8 +105,8 @@ class Dingus {
    * @param {string} urlPath 
    * @param {fn} handler 
    */
-  on(method, urlPath, handler) {
-    this.router.on(method, urlPath, handler);
+  on(method, urlPath, handler, ...handlerArgs) {
+    this.router.on(method, urlPath, handler, handlerArgs);
   }
 
 
@@ -257,9 +259,9 @@ class Dingus {
     const { pathPart, queryParams } = this._splitUrl(req.url);
     ctx.queryParams = queryParams;
 
-    let handler;
+    let handler, handlerArgs = [];
     try {
-      handler = this.router.lookup(req.method, pathPart, ctx);
+      ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
     } catch (e) {
       if (e instanceof DingusError) {
         switch (e.message) {
@@ -272,7 +274,7 @@ class Dingus {
           default:
             this.logger.error(_scope, 'unknown dingus error', { error: e });
             handler = this.handlerInternalServerError.bind(this);
-          }
+        }
       } else if (e instanceof URIError) {
         handler = this.handlerBadRequest.bind(this);
       } else {
@@ -283,7 +285,7 @@ class Dingus {
 
     try {
       await this.preHandler(req, res, ctx);
-      return await handler(req, res, ctx);
+      return await handler(req, res, ctx, ...handlerArgs);
     } catch (e) {
       ctx.error = e;
       this.sendErrorResponse(e, req, res, ctx);
@@ -444,6 +446,41 @@ class Dingus {
   }
 
 
+  /**
+   * Potentially add additional headers from static file meta-file.
+   * @param {http.ServerResponse} res
+   * @param {string} directory
+   * @param {string} fileName - already normalized and filtered
+   */
+  async _serveFileMetaHeaders(res, directory, fileName) {
+    const _scope = _fileScope('_serveFileMetaHeaders');
+    this.logger.debug(_scope, 'called', { directory, fileName });
+
+    const metaPrefix = '.';
+    const metaSuffix = '.meta';
+    const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`;
+    const metaFilePath = path.join(directory, metaFileName);
+
+    const [stat, data] = await this._readFileInfo(metaFilePath);
+    if (!stat) {
+      return;
+    }
+
+    const lineBreakRE = /\r\n|\n|\r/;
+    const lines = data.toString().split(lineBreakRE);
+    common.unfoldHeaderLines(lines);
+
+    const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
+    lines.forEach((line) => {
+      if (line) {
+        const result = headerParseRE.exec(line);
+        const { groups: header } = result;
+        res.setHeader(header.name, header.value);
+      }
+    });
+  }
+
+
   /**
    * Serve a file from a directory, with rudimentary cache awareness.
    * This will also serve pre-encoded variations if available and requested.
@@ -524,6 +561,10 @@ class Dingus {
     // We presume static files are relatively cacheable.
     res.setHeader(Enum.Header.CacheControl, 'public');
 
+    if (this.staticMetadata) {
+      await this._serveFileMetaHeaders(res, directory, fileName);
+    }
+
     this.logger.debug(_scope, 'serving file', { filePath, contentType });
     res.end(data);
   }
@@ -583,6 +624,37 @@ class Dingus {
   }
 
 
+  /**
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {object} ctx
+   * @param {String} file - override ctx.params.file
+   */
+  async handlerGetStaticFile(req, res, ctx, file) {
+    Dingus.setHeadHandler(req, res, ctx);
+
+    // Set a default response type to handle any errors; will be re-set to serve actual static content type.
+    this.setResponseType(this.responseTypes, req, res, ctx);
+
+    await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file);
+  }
+
+
+  /**
+   * @param {http.ClientRequest} req
+   * @param {http.ServerResponse} res
+   * @param {Object} ctx
+   * @param {String} newPath
+   * @param {Number} statusCode
+  */
+  async handlerRedirect(req, res, ctx, newPath, statusCode = 307) {
+    this.setResponseType(this.responseTypes, req, res, ctx);
+    res.setHeader(Enum.Header.Location, newPath);
+    res.statusCode = statusCode;
+    res.end();
+  }
+
+
   /**
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
@@ -610,17 +682,18 @@ class Dingus {
    * @param {http.ServerResponse} res
    * @param {object} ctx
    */
-   async handlerBadRequest(req, res, ctx) {
+  async handlerBadRequest(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);
     throw new ResponseError(Enum.ErrorResponse.BadRequest);
   }
 
+
   /**
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {object} ctx
    */
-   async handlerInternalServerError(req, res, ctx) {
+  async handlerInternalServerError(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);
     throw new ResponseError(Enum.ErrorResponse.InternalServerError);
   }