Projekt

Obecné

Profil

Stáhnout (5.84 KB) Statistiky
| Větev: | Revize:
1 3a515b92 cagy
/*!
2
 * compression
3
 * Copyright(c) 2010 Sencha Inc.
4
 * Copyright(c) 2011 TJ Holowaychuk
5
 * Copyright(c) 2014 Jonathan Ong
6
 * Copyright(c) 2014-2015 Douglas Christopher Wilson
7
 * MIT Licensed
8
 */
9
10
'use strict'
11
12
/**
13
 * Module dependencies.
14
 * @private
15
 */
16
17
var accepts = require('accepts')
18
var Buffer = require('safe-buffer').Buffer
19
var bytes = require('bytes')
20
var compressible = require('compressible')
21
var debug = require('debug')('compression')
22
var onHeaders = require('on-headers')
23
var vary = require('vary')
24
var zlib = require('zlib')
25
26
/**
27
 * Module exports.
28
 */
29
30
module.exports = compression
31
module.exports.filter = shouldCompress
32
33
/**
34
 * Module variables.
35
 * @private
36
 */
37
38
var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
39
40
/**
41
 * Compress response data with gzip / deflate.
42
 *
43
 * @param {Object} [options]
44
 * @return {Function} middleware
45
 * @public
46
 */
47
48
function compression (options) {
49
  var opts = options || {}
50
51
  // options
52
  var filter = opts.filter || shouldCompress
53
  var threshold = bytes.parse(opts.threshold)
54
55
  if (threshold == null) {
56
    threshold = 1024
57
  }
58
59
  return function compression (req, res, next) {
60
    var ended = false
61
    var length
62
    var listeners = []
63
    var stream
64
65
    var _end = res.end
66
    var _on = res.on
67
    var _write = res.write
68
69
    // flush
70
    res.flush = function flush () {
71
      if (stream) {
72
        stream.flush()
73
      }
74
    }
75
76
    // proxy
77
78
    res.write = function write (chunk, encoding) {
79
      if (ended) {
80
        return false
81
      }
82
83
      if (!this._header) {
84
        this._implicitHeader()
85
      }
86
87
      return stream
88
        ? stream.write(toBuffer(chunk, encoding))
89
        : _write.call(this, chunk, encoding)
90
    }
91
92
    res.end = function end (chunk, encoding) {
93
      if (ended) {
94
        return false
95
      }
96
97
      if (!this._header) {
98
        // estimate the length
99
        if (!this.getHeader('Content-Length')) {
100
          length = chunkLength(chunk, encoding)
101
        }
102
103
        this._implicitHeader()
104
      }
105
106
      if (!stream) {
107
        return _end.call(this, chunk, encoding)
108
      }
109
110
      // mark ended
111
      ended = true
112
113
      // write Buffer for Node.js 0.8
114
      return chunk
115
        ? stream.end(toBuffer(chunk, encoding))
116
        : stream.end()
117
    }
118
119
    res.on = function on (type, listener) {
120
      if (!listeners || type !== 'drain') {
121
        return _on.call(this, type, listener)
122
      }
123
124
      if (stream) {
125
        return stream.on(type, listener)
126
      }
127
128
      // buffer listeners for future stream
129
      listeners.push([type, listener])
130
131
      return this
132
    }
133
134
    function nocompress (msg) {
135
      debug('no compression: %s', msg)
136
      addListeners(res, _on, listeners)
137
      listeners = null
138
    }
139
140
    onHeaders(res, function onResponseHeaders () {
141
      // determine if request is filtered
142
      if (!filter(req, res)) {
143
        nocompress('filtered')
144
        return
145
      }
146
147
      // determine if the entity should be transformed
148
      if (!shouldTransform(req, res)) {
149
        nocompress('no transform')
150
        return
151
      }
152
153
      // vary
154
      vary(res, 'Accept-Encoding')
155
156
      // content-length below threshold
157
      if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) {
158
        nocompress('size below threshold')
159
        return
160
      }
161
162
      var encoding = res.getHeader('Content-Encoding') || 'identity'
163
164
      // already encoded
165
      if (encoding !== 'identity') {
166
        nocompress('already encoded')
167
        return
168
      }
169
170
      // head
171
      if (req.method === 'HEAD') {
172
        nocompress('HEAD request')
173
        return
174
      }
175
176
      // compression method
177
      var accept = accepts(req)
178
      var method = accept.encoding(['gzip', 'deflate', 'identity'])
179
180
      // we really don't prefer deflate
181
      if (method === 'deflate' && accept.encoding(['gzip'])) {
182
        method = accept.encoding(['gzip', 'identity'])
183
      }
184
185
      // negotiation failed
186
      if (!method || method === 'identity') {
187
        nocompress('not acceptable')
188
        return
189
      }
190
191
      // compression stream
192
      debug('%s compression', method)
193
      stream = method === 'gzip'
194
        ? zlib.createGzip(opts)
195
        : zlib.createDeflate(opts)
196
197
      // add buffered listeners to stream
198
      addListeners(stream, stream.on, listeners)
199
200
      // header fields
201
      res.setHeader('Content-Encoding', method)
202
      res.removeHeader('Content-Length')
203
204
      // compression
205
      stream.on('data', function onStreamData (chunk) {
206
        if (_write.call(res, chunk) === false) {
207
          stream.pause()
208
        }
209
      })
210
211
      stream.on('end', function onStreamEnd () {
212
        _end.call(res)
213
      })
214
215
      _on.call(res, 'drain', function onResponseDrain () {
216
        stream.resume()
217
      })
218
    })
219
220
    next()
221
  }
222
}
223
224
/**
225
 * Add bufferred listeners to stream
226
 * @private
227
 */
228
229
function addListeners (stream, on, listeners) {
230
  for (var i = 0; i < listeners.length; i++) {
231
    on.apply(stream, listeners[i])
232
  }
233
}
234
235
/**
236
 * Get the length of a given chunk
237
 */
238
239
function chunkLength (chunk, encoding) {
240
  if (!chunk) {
241
    return 0
242
  }
243
244
  return !Buffer.isBuffer(chunk)
245
    ? Buffer.byteLength(chunk, encoding)
246
    : chunk.length
247
}
248
249
/**
250
 * Default filter function.
251
 * @private
252
 */
253
254
function shouldCompress (req, res) {
255
  var type = res.getHeader('Content-Type')
256
257
  if (type === undefined || !compressible(type)) {
258
    debug('%s not compressible', type)
259
    return false
260
  }
261
262
  return true
263
}
264
265
/**
266
 * Determine if the entity should be transformed.
267
 * @private
268
 */
269
270
function shouldTransform (req, res) {
271
  var cacheControl = res.getHeader('Cache-Control')
272
273
  // Don't compress for Cache-Control: no-transform
274
  // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
275
  return !cacheControl ||
276
    !cacheControlNoTransformRegExp.test(cacheControl)
277
}
278
279
/**
280
 * Coerce arguments to Buffer
281
 * @private
282
 */
283
284
function toBuffer (chunk, encoding) {
285
  return !Buffer.isBuffer(chunk)
286
    ? Buffer.from(chunk, encoding)
287
    : chunk
288
}