potentially serve static files with static headers
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 7 Aug 2021 18:59:22 +0000 (11:59 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sun, 8 Aug 2021 00:57:33 +0000 (17:57 -0700)
Support setting additional headers when serving static files by reading
from an associated metadata file.  Reminiscent of a CERN-era feature.

.markdownlint.json [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
lib/common.js
lib/dingus.js
package-lock.json
package.json
test/lib/common.js
test/lib/dingus.js

diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644 (file)
index 0000000..18e3c71
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "MD013": false,
+  "MD024": false
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..4b796d1
--- /dev/null
@@ -0,0 +1,23 @@
+# Changelog
+
+Releases and notable changes to this project are documented here.
+
+## [Unreleased]
+
+## [v1.1.0] - 
+
+### Added
+
+- support setting additional static headers when serving static files
+
+## [v1.0.0] - 2021-08-01
+
+### Added
+
+- Everything.
+
+---
+
+[Unreleased]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=HEAD;hp=v1.1.0
+[v1.1.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.1.0;hp=v1.0.0
+[v1.0.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.0.0;hp=v0.0.0
\ No newline at end of file
index 284427a7bd769742eafda2838b66275951a25de3..ff68e9d7ac7b5ea5258ce1aab70cb99f991d037f 100644 (file)
@@ -227,6 +227,10 @@ const nullLogger = {
   debug: nop,
 };
 
+/**
+ * Populates any absent logger levels.
+ * @param {Object} logger
+ */
 const ensureLoggerLevels = (logger = {}) => {
   for (const level in nullLogger) {
     if (! (level in logger)) {
@@ -236,22 +240,43 @@ const ensureLoggerLevels = (logger = {}) => {
   return logger;
 };
 
+/**
+ * Merges folded header lines
+ * @param {String[]} lines
+ */
+const unfoldHeaderLines = (lines) => {
+  const foldedLineRE = /^(\t| +)(.*)$/;
+  if (lines) {
+    lines.reduceRight((_, line, idx) => {
+      const result = foldedLineRE.exec(line);
+      if (result && idx) {
+        const prevIdx = idx - 1;
+        const mergedLine = `${lines[prevIdx]} ${result[2]}`;
+        lines.splice(prevIdx, 2, mergedLine);
+        return mergedLine;
+      }
+    }, null);
+  }
+  return lines;
+};
+
 module.exports = {
+  ensureLoggerLevels,
   fileScope,
   generateETag,
   get,
   handlerLogData,
-  isClientCached,
   httpStatusCodeClass,
+  isClientCached,
   mergeDeep,
   mergeEnum,
   nop,
   nullLogger,
-  ensureLoggerLevels,
   pick,
   requestId,
   requestLogData,
   responseLogData,
   setOptions,
   splitFirst,
+  unfoldHeaderLines,
 };
index c8e4909bf1edca17202e1ca26a3310b9d748ffb3..1cbfd72b8c95ad1d3fe3de38f7a1840349887a15 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
    */
@@ -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);
   }
@@ -615,6 +656,7 @@ class Dingus {
     throw new ResponseError(Enum.ErrorResponse.BadRequest);
   }
 
+
   /**
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
index e32d0ce50c6c0f2c9ee4f27089a7b5345b35b2f5..3296492b0d2fea77388535c406bff5b89e010f50 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/api-dingus",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
     },
     "y18n": {
       "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
-      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+      "resolved": "",
       "dev": true
     },
     "yallist": {
index 4202607ad805a78bfdf8dce563d24540b9b9489d..3b1199c59a476f1129bd6c4e843f055b096cc63c 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/api-dingus",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "description": "A minimal API server framework",
   "main": "index.js",
   "scripts": {
index b802003af74c0a50d5a9ecf538c364df8eb682b7..a04bf614eeadac136f2df8ff44cf1519846d8221 100644 (file)
@@ -258,7 +258,6 @@ describe('common', function () {
     });
   }); // requestId
 
-
   describe('ensureLoggerLevels', function () {
     it('adds missing levels', function () {
       const result = common.ensureLoggerLevels();
@@ -378,4 +377,26 @@ describe('common', function () {
     });
   }); // mergeDeep
 
+  describe('unfoldHeaderLines', function () {
+    it('folds', function () {
+      const lines = [
+        'Normal-Header: some header data',
+        'Folded-Header: more data',
+        '   second line of data',
+        '   third line of data',
+      ];
+      const expected = [
+        'Normal-Header: some header data',
+        'Folded-Header: more data second line of data third line of data',
+      ];
+      const result = common.unfoldHeaderLines(lines);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers no input', function () {
+      const lines = undefined;
+      const result = common.unfoldHeaderLines();
+      assert.deepStrictEqual(result, lines);
+    });
+  }); // unfoldHeaderLines
+
 });
index a1070a558c534a24c4f2d80a529e24bd8b7e64ed..eeeaf3a8980bf17eb342833b0722721c96931905 100644 (file)
@@ -13,8 +13,10 @@ const Enum = require('../../lib/enum');
 const noExpectedException = 'did not get expected exception';
 
 describe('Dingus', function () {
-  const dingus = new Dingus();
-
+  let dingus;
+  beforeEach(function () {
+    dingus = new Dingus();
+  });
   afterEach(function () {
     sinon.restore();
   });
@@ -224,7 +226,7 @@ describe('Dingus', function () {
       dingus.on('GET', '/', () => {});
       assert(stubOn.called);
     });
-  });
+  }); // on
 
   describe('setEndBodyHandler', function () {
     let req, res, ctx, handler, origEnd, origWrite;
@@ -591,6 +593,33 @@ describe('Dingus', function () {
     });
   }); // _readFileInfo
 
+  describe('_serveFileMetaHeaders', function () {
+    let res, directory, fileName;
+    beforeEach(function () {
+      sinon.stub(dingus, '_readFileInfo');
+      res = {
+        setHeader: sinon.stub(),
+      };
+      directory = '/path';
+      fileName = 'filename';
+    });
+    it('covers no meta file', async function() {
+      dingus._readFileInfo.resolves([null, null]);
+      await dingus._serveFileMetaHeaders(res, directory, fileName);
+      assert(!res.setHeader.called);
+    });
+    it('adds extra headers', async function () {
+      dingus._readFileInfo.resolves([{}, Buffer.from(`Link: <https://example.com/>; rel="relation"
+X-Folded-Header: data
+  data under
+  the fold
+Content-Type: image/sgi
+`)]);
+      await dingus._serveFileMetaHeaders(res, directory, fileName);
+      assert(res.setHeader.called);
+    });
+  }); // _serveFileMetaHeaders
+
   describe('serveFile', function () {
     const path = require('path');
     let ctx, req, res, directory, fileName, filestats;
@@ -649,6 +678,12 @@ describe('Dingus', function () {
       assert(fs.promises.readFile.called);
       assert(!dingus.handlerNotFound.called);
     });
+    it('covers no meta headers', async function () {
+      dingus.staticMetadata = false;
+      await dingus.serveFile(req, res, ctx, directory, fileName);
+      assert(fs.promises.readFile.called);
+      assert(!dingus.handlerNotFound.called);
+    });
     it('does not serve dot-file', async function () {
       fileName = '.example';
       await dingus.serveFile(req, res, ctx, directory, fileName);