diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index d1dec8714..2b01f84cf 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -31,7 +31,7 @@ const getTar = async () => { exports.expressCreateServer = async (hookName, args) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); - args.app.all(/\/javascripts\/(.*)/, assetCache.handle); + args.app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index d5d3de4fe..3cc4daf27 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -16,13 +16,14 @@ * limitations under the License. */ -const async = require('async'); const Buffer = require('buffer').Buffer; const fs = require('fs'); +const fsp = fs.promises; const path = require('path'); const zlib = require('zlib'); const settings = require('./Settings'); const existsSync = require('./path_exists'); +const util = require('util'); /* * The crypto module can be absent on reduced node installations. @@ -79,6 +80,10 @@ if (_crypto) { module.exports = class CachingMiddleware { handle(req, res, next) { + this._handle(req, res, next).catch((err) => next(err || new Error(err))); + } + + async _handle(req, res, next) { if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) { return next(undefined, req, res); } @@ -92,125 +97,107 @@ module.exports = class CachingMiddleware { const url = new URL(req.url, 'http://localhost'); const cacheKey = generateCacheKey(url.pathname + url.search); - fs.stat(`${CACHE_DIR}minified_${cacheKey}`, (error, stats) => { - const modifiedSince = (req.headers['if-modified-since'] && - new Date(req.headers['if-modified-since'])); - const lastModifiedCache = !error && stats.mtime; - if (lastModifiedCache && responseCache[cacheKey]) { - req.headers['if-modified-since'] = lastModifiedCache.toUTCString(); + const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {}); + const modifiedSince = + req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']); + if (stats != null && stats.mtime && responseCache[cacheKey]) { + req.headers['if-modified-since'] = stats.mtime.toUTCString(); + } else { + delete req.headers['if-modified-since']; + } + + // Always issue get to downstream. + oldReq.method = req.method; + req.method = 'GET'; + + // This handles read/write synchronization as well as its predecessor, + // which is to say, not at all. + // TODO: Implement locking on write or ditch caching of gzip and use + // existing middlewares. + const respond = () => { + req.method = oldReq.method || req.method; + res.write = oldRes.write || res.write; + res.end = oldRes.end || res.end; + + const headers = {}; + Object.assign(headers, (responseCache[cacheKey].headers || {})); + const statusCode = responseCache[cacheKey].statusCode; + + let pathStr = `${CACHE_DIR}minified_${cacheKey}`; + if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { + pathStr += '.gz'; + headers['content-encoding'] = 'gzip'; + } + + const lastModified = headers['last-modified'] && new Date(headers['last-modified']); + + if (statusCode === 200 && lastModified <= modifiedSince) { + res.writeHead(304, headers); + res.end(); + } else if (req.method === 'GET') { + const readStream = fs.createReadStream(pathStr); + res.writeHead(statusCode, headers); + readStream.pipe(res); } else { - delete req.headers['if-modified-since']; + res.writeHead(statusCode, headers); + res.end(); } + }; - // Always issue get to downstream. - oldReq.method = req.method; - req.method = 'GET'; + const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); + if (expirationDate > new Date()) { + // Our cached version is still valid. + return respond(); + } - // This handles read/write synchronization as well as its predecessor, - // which is to say, not at all. - // TODO: Implement locking on write or ditch caching of gzip and use - // existing middlewares. - const respond = () => { - req.method = oldReq.method || req.method; - res.write = oldRes.write || res.write; - res.end = oldRes.end || res.end; + const _headers = {}; + oldRes.setHeader = res.setHeader; + res.setHeader = (key, value) => { + // Don't set cookies, see issue #707 + if (key.toLowerCase() === 'set-cookie') return; - const headers = {}; - Object.assign(headers, (responseCache[cacheKey].headers || {})); - const statusCode = responseCache[cacheKey].statusCode; + _headers[key.toLowerCase()] = value; + oldRes.setHeader.call(res, key, value); + }; - let pathStr = `${CACHE_DIR}minified_${cacheKey}`; - if (supportsGzip && /application\/javascript/.test(headers['content-type'])) { - pathStr += '.gz'; - headers['content-encoding'] = 'gzip'; - } + oldRes.writeHead = res.writeHead; + res.writeHead = (status, headers) => { + res.writeHead = oldRes.writeHead; + if (status === 200) { + // Update cache + let buffer = ''; - const lastModified = (headers['last-modified'] && - new Date(headers['last-modified'])); + Object.keys(headers || {}).forEach((key) => { + res.setHeader(key, headers[key]); + }); + headers = _headers; - if (statusCode === 200 && lastModified <= modifiedSince) { - res.writeHead(304, headers); - res.end(); - } else if (req.method === 'GET') { - const readStream = fs.createReadStream(pathStr); - res.writeHead(statusCode, headers); - readStream.pipe(res); - } else { - res.writeHead(statusCode, headers); - res.end(); - } - }; - - const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires); - if (expirationDate > new Date()) { - // Our cached version is still valid. - return respond(); + oldRes.write = res.write; + oldRes.end = res.end; + res.write = (data, encoding) => { + buffer += data.toString(encoding); + }; + res.end = async (data, encoding) => { + await Promise.all([ + fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}), + util.promisify(zlib.gzip)(buffer) + .then((content) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content)) + .catch(() => {}), + ]); + responseCache[cacheKey] = {statusCode: status, headers}; + respond(); + }; + } else if (status === 304) { + // Nothing new changed from the cached version. + oldRes.write = res.write; + oldRes.end = res.end; + res.write = (data, encoding) => {}; + res.end = (data, encoding) => { respond(); }; + } else { + res.writeHead(status, headers); } + }; - const _headers = {}; - oldRes.setHeader = res.setHeader; - res.setHeader = (key, value) => { - // Don't set cookies, see issue #707 - if (key.toLowerCase() === 'set-cookie') return; - - _headers[key.toLowerCase()] = value; - oldRes.setHeader.call(res, key, value); - }; - - oldRes.writeHead = res.writeHead; - res.writeHead = (status, headers) => { - res.writeHead = oldRes.writeHead; - if (status === 200) { - // Update cache - let buffer = ''; - - Object.keys(headers || {}).forEach((key) => { - res.setHeader(key, headers[key]); - }); - headers = _headers; - - oldRes.write = res.write; - oldRes.end = res.end; - res.write = (data, encoding) => { - buffer += data.toString(encoding); - }; - res.end = (data, encoding) => { - async.parallel([ - (callback) => { - const path = `${CACHE_DIR}minified_${cacheKey}`; - fs.writeFile(path, buffer, (error, stats) => { - callback(); - }); - }, - (callback) => { - const path = `${CACHE_DIR}minified_${cacheKey}.gz`; - zlib.gzip(buffer, (error, content) => { - if (error) { - callback(); - } else { - fs.writeFile(path, content, (error, stats) => { - callback(); - }); - } - }); - }, - ], () => { - responseCache[cacheKey] = {statusCode: status, headers}; - respond(); - }); - }; - } else if (status === 304) { - // Nothing new changed from the cached version. - oldRes.write = res.write; - oldRes.end = res.end; - res.write = (data, encoding) => {}; - res.end = (data, encoding) => { respond(); }; - } else { - res.writeHead(status, headers); - } - }; - - next(undefined, req, res); - }); + next(undefined, req, res); } };