--- /dev/null
+{
+ "MD013": false,
+ "MD024": false
+}
--- /dev/null
+# 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
debug: nop,
};
+/**
+ * Populates any absent logger levels.
+ * @param {Object} logger
+ */
const ensureLoggerLevels = (logger = {}) => {
for (const level in nullLogger) {
if (! (level in 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,
};
proxyPrefix: '',
strictAccept: true,
selfBaseUrl: '',
+ staticMetadata: true,
trustProxy: true,
querystring,
};
* @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
*/
}
+ /**
+ * 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.
// 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);
}
throw new ResponseError(Enum.ErrorResponse.BadRequest);
}
+
/**
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
{
"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": {
{
"name": "@squeep/api-dingus",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "A minimal API server framework",
"main": "index.js",
"scripts": {
});
}); // requestId
-
describe('ensureLoggerLevels', function () {
it('adds missing levels', function () {
const result = common.ensureLoggerLevels();
});
}); // 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
+
});
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();
});
dingus.on('GET', '/', () => {});
assert(stubOn.called);
});
- });
+ }); // on
describe('setEndBodyHandler', function () {
let req, res, ctx, handler, origEnd, origWrite;
});
}); // _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;
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);