@@ -6,8 +6,14 @@ var crypto = require('crypto')
6
6
var debug = require ( 'debug' ) ( 'web:cache' )
7
7
var path = require ( 'path' )
8
8
var url = require ( 'url' )
9
+ var fs = require ( 'fs' )
10
+
11
+ var compressible = require ( 'compressible' )
12
+ var mime = require ( 'mime-types' )
13
+ var etag = require ( 'etag' )
9
14
10
15
var config = require ( path . join ( __dirname , '/../../../config.js' ) )
16
+ var help = require ( path . join ( __dirname , '/../help' ) )
11
17
12
18
var DadiCache = require ( '@dadi/cache' )
13
19
@@ -20,9 +26,6 @@ var Cache = function (server) {
20
26
this . server = server
21
27
this . cache = new DadiCache ( config . get ( 'caching' ) )
22
28
23
- Cache . numInstances = ( Cache . numInstances || 0 ) + 1
24
- // console.log('Cache:', Cache.numInstances)
25
-
26
29
var directoryEnabled = config . get ( 'caching.directory.enabled' )
27
30
var redisEnabled = config . get ( 'caching.redis.enabled' )
28
31
@@ -46,31 +49,29 @@ module.exports = function (server) {
46
49
* @returns {Boolean }
47
50
*/
48
51
Cache . prototype . cachingEnabled = function ( req ) {
52
+ // Check it is not a json view
49
53
var query = url . parse ( req . url , true ) . query
50
- if ( query . json && query . json !== 'false' ) {
51
- return false
52
- }
54
+ if ( query . json && query . json !== 'false' ) return false
53
55
54
- if ( config . get ( 'debug' ) ) {
55
- return false
56
- }
56
+ // Disable cache for debug mode
57
+ if ( config . get ( 'debug' ) ) return false
57
58
58
- var endpoint = this . getEndpointMatchingRequest ( req )
59
+ // if it's in the endpoint and caching is enabled
60
+ var endpoint = this . getEndpoint ( req )
59
61
60
- if ( ! endpoint ) {
61
- endpoint = this . getEndpointMatchingLoadedPaths ( req )
62
- }
62
+ if ( endpoint ) {
63
+ this . options . cache =
64
+ typeof endpoint . page . settings . cache !== 'undefined'
65
+ ? endpoint . page . settings . cache
66
+ : this . enabled
63
67
64
- // not found in the loaded routes, let's not bother caching
65
- if ( ! endpoint ) return false
66
-
67
- if ( endpoint . page && endpoint . page . settings ) {
68
- this . options = endpoint . page . settings
68
+ return this . enabled && this . options . cache
69
69
} else {
70
- this . options . cache = false
71
- }
70
+ // Otherwise it might be in the public folder
71
+ var file = url . parse ( req . url ) . pathname
72
72
73
- return this . enabled && ( this . options . cache || false )
73
+ return compressible ( mime . lookup ( file ) )
74
+ }
74
75
}
75
76
76
77
/**
@@ -80,12 +81,7 @@ Cache.prototype.cachingEnabled = function (req) {
80
81
*/
81
82
Cache . prototype . getEndpointMatchingRequest = function ( req ) {
82
83
var endpoints = this . server . components
83
- var requestUrl = url . parse ( req . url , true ) . pathname
84
-
85
- // strip trailing slash before testing
86
- if ( requestUrl !== '/' && requestUrl [ requestUrl . length - 1 ] === '/' ) {
87
- requestUrl = requestUrl . substring ( 0 , requestUrl . length - 1 )
88
- }
84
+ var requestUrl = url . parse ( req . url , true ) . pathname . replace ( / \/ + $ / , '' )
89
85
90
86
// get the host key that matches the request's host header
91
87
var virtualHosts = config . get ( 'virtualHosts' )
@@ -96,7 +92,7 @@ Cache.prototype.getEndpointMatchingRequest = function (req) {
96
92
} ) || ''
97
93
98
94
// check if there is a match in the loaded routes for the current request URL
99
- var endpoint = _ . find ( endpoints , endpoint => {
95
+ return _ . find ( endpoints , endpoint => {
100
96
var paths = _ . pluck ( endpoint . page . routes , 'path' )
101
97
return (
102
98
_ . contains ( paths , requestUrl ) &&
@@ -105,9 +101,8 @@ Cache.prototype.getEndpointMatchingRequest = function (req) {
105
101
: true )
106
102
)
107
103
} )
108
-
109
- return endpoint
110
104
}
105
+
111
106
/**
112
107
* Retrieves the page component that best matches the paths loaded in api/index.js
113
108
* @param {IncomingMessage } req - the current HTTP request
@@ -130,14 +125,23 @@ Cache.prototype.getEndpointMatchingLoadedPaths = function (req) {
130
125
* @param {IncomingMessage } req - the current HTTP request
131
126
* @returns {string }
132
127
*/
133
- Cache . prototype . getEndpointContentType = function ( req ) {
134
- var endpoint = this . getEndpointMatchingRequest ( req )
128
+ Cache . prototype . getReqContentType = function ( req ) {
129
+ // Check for content-type in the page json
130
+ var endpoint = this . getEndpoint ( req )
135
131
136
- if ( ! endpoint ) {
137
- endpoint = this . getEndpointMatchingLoadedPaths ( req )
138
- }
132
+ return endpoint && endpoint . page && endpoint . page . contentType
133
+ ? endpoint . page . contentType
134
+ : false
135
+ }
136
+
137
+ /**
138
+ * Adds the Cache middleware to the stack
139
+ */
140
+ Cache . prototype . getEndpoint = function ( req ) {
141
+ var endpoint = this . getEndpointMatchingRequest ( req )
142
+ if ( ! endpoint ) endpoint = this . getEndpointMatchingLoadedPaths ( req )
139
143
140
- return endpoint . page . contentType
144
+ return endpoint
141
145
}
142
146
143
147
/**
@@ -154,9 +158,15 @@ Cache.prototype.init = function () {
154
158
this . server . app . use ( function cache ( req , res , next ) {
155
159
var enabled = self . cachingEnabled ( req )
156
160
161
+ if ( ! enabled ) return next ( )
162
+
157
163
debug ( '%s%s, cache enabled: %s' , req . headers . host , req . url , enabled )
158
164
159
- if ( ! enabled ) return next ( )
165
+ // Check it's a page
166
+ if ( ! self . getEndpoint ( req ) ) return next ( )
167
+
168
+ // get contentType that current endpoint requires
169
+ var contentType = self . getReqContentType ( req )
160
170
161
171
// only cache GET requests
162
172
if ( req . method && req . method . toLowerCase ( ) !== 'get' ) return next ( )
@@ -183,33 +193,63 @@ Cache.prototype.init = function () {
183
193
var noCache =
184
194
query . cache && query . cache . toString ( ) . toLowerCase ( ) === 'false'
185
195
186
- // get contentType that current endpoint requires
187
- var contentType = self . getEndpointContentType ( req )
196
+ // File extension for cache file
197
+ var cacheExt =
198
+ compressible ( contentType ) && help . canCompress ( req . headers )
199
+ ? '.' + help . canCompress ( req . headers )
200
+ : null
201
+
202
+ var opts = {
203
+ directory : { extension : mime . extension ( contentType ) + cacheExt }
204
+ }
205
+
206
+ // Compression settings
207
+ var shouldCompress = compressible ( contentType )
208
+ ? help . canCompress ( req . headers )
209
+ : false
188
210
189
211
// attempt to get from the cache
190
212
self . cache
191
- . get ( filename )
213
+ . get ( filename , opts )
192
214
. then ( stream => {
193
215
debug ( 'serving %s%s from cache' , req . headers . host , req . url )
194
216
195
- res . setHeader ( 'X-Cache-Lookup' , 'HIT' )
196
-
197
217
if ( noCache ) {
218
+ res . setHeader ( 'X-Cache-Lookup' , 'HIT' )
198
219
res . setHeader ( 'X-Cache' , 'MISS' )
199
220
return next ( )
200
221
}
201
222
223
+ var headers = {
224
+ 'X-Cache-Lookup' : 'HIT' ,
225
+ 'X-Cache' : 'HIT' ,
226
+ 'Content-Type' : contentType ,
227
+ 'Cache-Control' :
228
+ config . get ( 'headers.cacheControl' ) [ contentType ] ||
229
+ 'public, max-age=86400'
230
+ }
231
+
232
+ // Add compression headers
233
+ if ( shouldCompress ) headers [ 'Content-Encoding' ] = shouldCompress
234
+
235
+ // Add extra headers
236
+ stream . on ( 'open' , fd => {
237
+ fs . fstat ( fd , ( _ , stats ) => {
238
+ res . setHeader ( 'Content-Length' , stats . size )
239
+ res . setHeader ( 'ETag' , etag ( stats ) )
240
+ } )
241
+ } )
242
+
202
243
res . statusCode = 200
203
- res . setHeader ( 'X-Cache' , 'HIT' )
204
- res . setHeader ( 'Content-Type' , contentType )
244
+ Object . keys ( headers ) . map ( i => res . setHeader ( i , headers [ i ] ) )
205
245
206
- // send cached content back
207
246
stream . pipe ( res )
208
247
} )
209
248
. catch ( ( ) => {
210
249
// not found in cache
211
250
res . setHeader ( 'X-Cache' , 'MISS' )
212
251
res . setHeader ( 'X-Cache-Lookup' , 'MISS' )
252
+
213
253
return cacheResponse ( )
214
254
} )
215
255
@@ -222,24 +262,31 @@ Cache.prototype.init = function () {
222
262
var _end = res . end
223
263
var _write = res . write
224
264
225
- var data = ''
265
+ var data = [ ]
226
266
227
267
res . write = function ( chunk ) {
268
+ if ( chunk ) data . push ( chunk )
269
+
228
270
_write . apply ( res , arguments )
229
271
}
230
272
231
273
res . end = function ( chunk ) {
232
274
// respond before attempting to cache
233
275
_end . apply ( res , arguments )
234
276
235
- if ( chunk ) data += chunk
277
+ if ( chunk && ! data . length ) data . push ( chunk )
236
278
237
279
// if response is not 200 don't cache
238
280
if ( res . statusCode !== 200 ) return
239
281
240
- // cache the content
241
- self . cache . set ( filename , data ) . then ( ( ) => { } )
282
+ // cache the content, with applicable file extension
283
+ try {
284
+ self . cache . set ( filename , Buffer . concat ( data ) , opts ) . then ( ( ) => { } )
285
+ } catch ( e ) {
286
+ console . log ( 'Could not cache content: ' + requestUrl )
287
+ }
242
288
}
289
+
243
290
return next ( )
244
291
}
245
292
} )
0 commit comments