From 0a673b319d91d69d373b2ea34a74a53a7080d092 Mon Sep 17 00:00:00 2001 From: Ryota Kameoka Date: Sun, 4 Oct 2020 07:21:54 +0900 Subject: [PATCH 1/3] Update supertest to v5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 22254f3..c358852 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "mocha": "2", "should": "8", "should-http": "0.0.4", - "supertest": "1", + "supertest": "5", "ylru": "1" }, "scripts": { From 309e3f7aea1b7276d080286e4cf4a4a35b42e2ad Mon Sep 17 00:00:00 2001 From: Ryota Kameoka Date: Sun, 4 Oct 2020 07:22:25 +0900 Subject: [PATCH 2/3] Fix broken test cases --- test/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/index.js b/test/index.js index 1edeb0f..a6c28b0 100644 --- a/test/index.js +++ b/test/index.js @@ -189,7 +189,7 @@ describe('Static Cache', function () { request(server) .head('/index.js') .expect(200) - .expect('', done) + .expect(undefined, done) }) it('should support 404 Not Found for other Methods to allow downstream', @@ -284,7 +284,7 @@ describe('Static Cache', function () { .expect('Cache-Control', 'public, max-age=0') .expect('Content-Encoding', 'gzip') .expect('Content-Type', /javascript/) - .expect('Content-Length', content.length) + .expect('Content-Length', String(content.length)) .expect('Vary', 'Accept-Encoding') .expect(index.toString()) .end(function (err, res) { @@ -310,7 +310,7 @@ describe('Static Cache', function () { .expect(200) .expect('Cache-Control', 'public, max-age=0') .expect('Content-Type', /javascript/) - .expect('Content-Length', index.length) + .expect('Content-Length', String(index.length)) .expect('Vary', 'Accept-Encoding') .expect(index.toString()) .end(function (err, res) { From 17b94f952e0fab5f724f5926b5d900322297f578 Mon Sep 17 00:00:00 2001 From: Ryota Kameoka Date: Sun, 4 Oct 2020 07:23:01 +0900 Subject: [PATCH 3/3] Support Brotli Closes #77 --- README.md | 2 + index.js | 34 ++++++++++++++--- test/index.js | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ba9afb8..7801bac 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ app.use(staticCache(path.join(__dirname, 'public'), { - `options.buffer` (bool) - store the files in memory instead of streaming from the filesystem on each request. - `options.gzip` (bool) - when request's accept-encoding include gzip, files will compressed by gzip. - `options.usePrecompiledGzip` (bool) - try use gzip files, loaded from disk, like nginx gzip_static +- `options.brotli` (bool) - when request's accept-encoding include br, files will compressed by brotli. +- `options.usePrecompiledBrotli` (bool) - try use brotli files, loaded from disk - `options.alias` (obj) - object map of aliases. See below. - `options.prefix` (str) - the url prefix you wish to add, default to `''`. - `options.dynamic` (bool) - dynamic load file which not cached on initialization. diff --git a/index.js b/index.js index 7b1f8d2..5efda2c 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ var mime = require('mime-types') var compressible = require('compressible') var readDir = require('fs-readdir-recursive') var debug = require('debug')('koa-static-cache') +var util = require('util'); module.exports = function staticCache(dir, options, files) { if (typeof dir === 'object') { @@ -20,6 +21,7 @@ module.exports = function staticCache(dir, options, files) { files = new FileManager(files || options.files) dir = dir || options.dir || process.cwd() dir = path.normalize(dir) + var enableBrotli = !!options.brotli var enableGzip = !!options.gzip var filePrefix = path.normalize(options.prefix.replace(/^\//, '')) @@ -79,7 +81,7 @@ module.exports = function staticCache(dir, options, files) { ctx.status = 200 - if (enableGzip) ctx.vary('Accept-Encoding') + if (enableBrotli || enableGzip) ctx.vary('Accept-Encoding') if (!file.buffer) { var stats = await fs.stat(file.path) @@ -102,10 +104,14 @@ module.exports = function staticCache(dir, options, files) { if (ctx.method === 'HEAD') return + var acceptBrotli = ctx.acceptsEncodings('br') === 'br' var acceptGzip = ctx.acceptsEncodings('gzip') === 'gzip' if (file.zipBuffer) { - if (acceptGzip) { + if (acceptBrotli) { + ctx.set('content-encoding', 'br') + ctx.body = file.brBuffer + } else if (acceptGzip) { ctx.set('content-encoding', 'gzip') ctx.body = file.zipBuffer } else { @@ -114,13 +120,27 @@ module.exports = function staticCache(dir, options, files) { return } + var shouldBrotli = enableBrotli + && file.length > 1024 + && acceptBrotli + && compressible(file.type) var shouldGzip = enableGzip && file.length > 1024 && acceptGzip && compressible(file.type) if (file.buffer) { - if (shouldGzip) { + if (shouldBrotli) { + + var brFile = files.get(filename + '.br') + if (options.usePrecompiledBrotli && brFile && brFile.buffer) { // if .br file already read from disk + file.brBuffer = brFile.buffer + } else { + file.brBuffer = await util.promisify(zlib.brotliCompress)(file.buffer) + } + ctx.set('content-encoding', 'br') + ctx.body = file.brBuffer + } else if (shouldGzip) { var gzFile = files.get(filename + '.gz') if (options.usePrecompiledGzip && gzFile && gzFile.buffer) { // if .gz file already read from disk @@ -148,8 +168,12 @@ module.exports = function staticCache(dir, options, files) { } ctx.body = stream - // enable gzip will remove content length - if (shouldGzip) { + // enable brotli/gzip will remove content length + if (shouldBrotli) { + ctx.remove('content-length') + ctx.set('content-encoding', 'br') + ctx.body = stream.pipe(zlib.createBrotliCompress()) + } else if (shouldGzip) { ctx.remove('content-length') ctx.set('content-encoding', 'gzip') ctx.body = stream.pipe(zlib.createGzip()) diff --git a/test/index.js b/test/index.js index a6c28b0..8d5f0d8 100644 --- a/test/index.js +++ b/test/index.js @@ -63,6 +63,26 @@ app5.use(staticCache({ })) var server5 = http.createServer(app5.callback()) +var app6 = new Koa() +app6.use(staticCache(path.join(__dirname, '..'), { + buffer: true, + brotli: true, + filter(file) { + return !file.includes('node_modules') + } +})) +var server6 = http.createServer(app6.callback()) + +var app7 = new Koa() +var files7 = {} +app7.use(staticCache(path.join(__dirname, '..'), { + brotli: true, + filter(file) { + return !file.includes('node_modules') + } +}, files7)) +var server7 = http.createServer(app7.callback()) + describe('Static Cache', function () { it('should dir priority than options.dir', function (done) { @@ -601,4 +621,85 @@ describe('Static Cache', function () { .expect(404) .end(done) }) + + it('should serve files with brotli buffer', function (done) { + var index = fs.readFileSync('index.js') + zlib.brotliCompress(index, function (err, content) { + request(server6) + .get('/index.js') + .set('Accept-Encoding', 'br') + .responseType('arraybuffer') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .expect('Content-Encoding', 'br') + .expect('Content-Type', /javascript/) + .expect('Content-Length', String(content.length)) + .expect('Vary', 'Accept-Encoding') + .expect(function (res) { + return res.body.toString('hex') === content.toString('hex') + }) + .end(function (err, res) { + if (err) + return done(err) + res.should.have.header('Content-Length') + res.should.have.header('Last-Modified') + res.should.have.header('ETag') + + etag = res.headers.etag + + done() + }) + }) + }) + + it('should not serve files with brotli buffer when accept encoding not include br', + function (done) { + var index = fs.readFileSync('index.js') + request(server6) + .get('/index.js') + .set('Accept-Encoding', '') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .expect('Content-Type', /javascript/) + .expect('Content-Length', String(index.length)) + .expect('Vary', 'Accept-Encoding') + .expect(index.toString()) + .end(function (err, res) { + if (err) + return done(err) + res.should.not.have.header('Content-Encoding') + res.should.have.header('Content-Length') + res.should.have.header('Last-Modified') + res.should.have.header('ETag') + done() + }) + }) + + it('should serve files with brotli stream', function (done) { + var index = fs.readFileSync('index.js') + zlib.brotliCompress(index, function (err, content) { + request(server7) + .get('/index.js') + .set('Accept-Encoding', 'br') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .expect('Content-Encoding', 'br') + .expect('Content-Type', /javascript/) + .expect('Vary', 'Accept-Encoding') + .expect(function (res) { + return res.body.toString('hex') === content.toString('hex') + }) + .end(function (err, res) { + if (err) + return done(err) + res.should.not.have.header('Content-Length') + res.should.have.header('Last-Modified') + res.should.have.header('ETag') + + etag = res.headers.etag + + done() + }) + }) + }) })