Skip to content

Commit 17b94f9

Browse files
committed
Support Brotli
Closes koajs#77
1 parent 309e3f7 commit 17b94f9

File tree

3 files changed

+132
-5
lines changed

3 files changed

+132
-5
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ app.use(staticCache(path.join(__dirname, 'public'), {
5050
- `options.buffer` (bool) - store the files in memory instead of streaming from the filesystem on each request.
5151
- `options.gzip` (bool) - when request's accept-encoding include gzip, files will compressed by gzip.
5252
- `options.usePrecompiledGzip` (bool) - try use gzip files, loaded from disk, like nginx gzip_static
53+
- `options.brotli` (bool) - when request's accept-encoding include br, files will compressed by brotli.
54+
- `options.usePrecompiledBrotli` (bool) - try use brotli files, loaded from disk
5355
- `options.alias` (obj) - object map of aliases. See below.
5456
- `options.prefix` (str) - the url prefix you wish to add, default to `''`.
5557
- `options.dynamic` (bool) - dynamic load file which not cached on initialization.

index.js

+29-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var mime = require('mime-types')
66
var compressible = require('compressible')
77
var readDir = require('fs-readdir-recursive')
88
var debug = require('debug')('koa-static-cache')
9+
var util = require('util');
910

1011
module.exports = function staticCache(dir, options, files) {
1112
if (typeof dir === 'object') {
@@ -20,6 +21,7 @@ module.exports = function staticCache(dir, options, files) {
2021
files = new FileManager(files || options.files)
2122
dir = dir || options.dir || process.cwd()
2223
dir = path.normalize(dir)
24+
var enableBrotli = !!options.brotli
2325
var enableGzip = !!options.gzip
2426
var filePrefix = path.normalize(options.prefix.replace(/^\//, ''))
2527

@@ -79,7 +81,7 @@ module.exports = function staticCache(dir, options, files) {
7981

8082
ctx.status = 200
8183

82-
if (enableGzip) ctx.vary('Accept-Encoding')
84+
if (enableBrotli || enableGzip) ctx.vary('Accept-Encoding')
8385

8486
if (!file.buffer) {
8587
var stats = await fs.stat(file.path)
@@ -102,10 +104,14 @@ module.exports = function staticCache(dir, options, files) {
102104

103105
if (ctx.method === 'HEAD') return
104106

107+
var acceptBrotli = ctx.acceptsEncodings('br') === 'br'
105108
var acceptGzip = ctx.acceptsEncodings('gzip') === 'gzip'
106109

107110
if (file.zipBuffer) {
108-
if (acceptGzip) {
111+
if (acceptBrotli) {
112+
ctx.set('content-encoding', 'br')
113+
ctx.body = file.brBuffer
114+
} else if (acceptGzip) {
109115
ctx.set('content-encoding', 'gzip')
110116
ctx.body = file.zipBuffer
111117
} else {
@@ -114,13 +120,27 @@ module.exports = function staticCache(dir, options, files) {
114120
return
115121
}
116122

123+
var shouldBrotli = enableBrotli
124+
&& file.length > 1024
125+
&& acceptBrotli
126+
&& compressible(file.type)
117127
var shouldGzip = enableGzip
118128
&& file.length > 1024
119129
&& acceptGzip
120130
&& compressible(file.type)
121131

122132
if (file.buffer) {
123-
if (shouldGzip) {
133+
if (shouldBrotli) {
134+
135+
var brFile = files.get(filename + '.br')
136+
if (options.usePrecompiledBrotli && brFile && brFile.buffer) { // if .br file already read from disk
137+
file.brBuffer = brFile.buffer
138+
} else {
139+
file.brBuffer = await util.promisify(zlib.brotliCompress)(file.buffer)
140+
}
141+
ctx.set('content-encoding', 'br')
142+
ctx.body = file.brBuffer
143+
} else if (shouldGzip) {
124144

125145
var gzFile = files.get(filename + '.gz')
126146
if (options.usePrecompiledGzip && gzFile && gzFile.buffer) { // if .gz file already read from disk
@@ -148,8 +168,12 @@ module.exports = function staticCache(dir, options, files) {
148168
}
149169

150170
ctx.body = stream
151-
// enable gzip will remove content length
152-
if (shouldGzip) {
171+
// enable brotli/gzip will remove content length
172+
if (shouldBrotli) {
173+
ctx.remove('content-length')
174+
ctx.set('content-encoding', 'br')
175+
ctx.body = stream.pipe(zlib.createBrotliCompress())
176+
} else if (shouldGzip) {
153177
ctx.remove('content-length')
154178
ctx.set('content-encoding', 'gzip')
155179
ctx.body = stream.pipe(zlib.createGzip())

test/index.js

+101
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,26 @@ app5.use(staticCache({
6363
}))
6464
var server5 = http.createServer(app5.callback())
6565

66+
var app6 = new Koa()
67+
app6.use(staticCache(path.join(__dirname, '..'), {
68+
buffer: true,
69+
brotli: true,
70+
filter(file) {
71+
return !file.includes('node_modules')
72+
}
73+
}))
74+
var server6 = http.createServer(app6.callback())
75+
76+
var app7 = new Koa()
77+
var files7 = {}
78+
app7.use(staticCache(path.join(__dirname, '..'), {
79+
brotli: true,
80+
filter(file) {
81+
return !file.includes('node_modules')
82+
}
83+
}, files7))
84+
var server7 = http.createServer(app7.callback())
85+
6686
describe('Static Cache', function () {
6787

6888
it('should dir priority than options.dir', function (done) {
@@ -601,4 +621,85 @@ describe('Static Cache', function () {
601621
.expect(404)
602622
.end(done)
603623
})
624+
625+
it('should serve files with brotli buffer', function (done) {
626+
var index = fs.readFileSync('index.js')
627+
zlib.brotliCompress(index, function (err, content) {
628+
request(server6)
629+
.get('/index.js')
630+
.set('Accept-Encoding', 'br')
631+
.responseType('arraybuffer')
632+
.expect(200)
633+
.expect('Cache-Control', 'public, max-age=0')
634+
.expect('Content-Encoding', 'br')
635+
.expect('Content-Type', /javascript/)
636+
.expect('Content-Length', String(content.length))
637+
.expect('Vary', 'Accept-Encoding')
638+
.expect(function (res) {
639+
return res.body.toString('hex') === content.toString('hex')
640+
})
641+
.end(function (err, res) {
642+
if (err)
643+
return done(err)
644+
res.should.have.header('Content-Length')
645+
res.should.have.header('Last-Modified')
646+
res.should.have.header('ETag')
647+
648+
etag = res.headers.etag
649+
650+
done()
651+
})
652+
})
653+
})
654+
655+
it('should not serve files with brotli buffer when accept encoding not include br',
656+
function (done) {
657+
var index = fs.readFileSync('index.js')
658+
request(server6)
659+
.get('/index.js')
660+
.set('Accept-Encoding', '')
661+
.expect(200)
662+
.expect('Cache-Control', 'public, max-age=0')
663+
.expect('Content-Type', /javascript/)
664+
.expect('Content-Length', String(index.length))
665+
.expect('Vary', 'Accept-Encoding')
666+
.expect(index.toString())
667+
.end(function (err, res) {
668+
if (err)
669+
return done(err)
670+
res.should.not.have.header('Content-Encoding')
671+
res.should.have.header('Content-Length')
672+
res.should.have.header('Last-Modified')
673+
res.should.have.header('ETag')
674+
done()
675+
})
676+
})
677+
678+
it('should serve files with brotli stream', function (done) {
679+
var index = fs.readFileSync('index.js')
680+
zlib.brotliCompress(index, function (err, content) {
681+
request(server7)
682+
.get('/index.js')
683+
.set('Accept-Encoding', 'br')
684+
.expect(200)
685+
.expect('Cache-Control', 'public, max-age=0')
686+
.expect('Content-Encoding', 'br')
687+
.expect('Content-Type', /javascript/)
688+
.expect('Vary', 'Accept-Encoding')
689+
.expect(function (res) {
690+
return res.body.toString('hex') === content.toString('hex')
691+
})
692+
.end(function (err, res) {
693+
if (err)
694+
return done(err)
695+
res.should.not.have.header('Content-Length')
696+
res.should.have.header('Last-Modified')
697+
res.should.have.header('ETag')
698+
699+
etag = res.headers.etag
700+
701+
done()
702+
})
703+
})
704+
})
604705
})

0 commit comments

Comments
 (0)