raspberryGPIOhtmlserver/node_modules/connect/lib/middleware/staticCache.js
node_modules/connect/lib/middleware/staticCache.jsRaw
1
2/*!
3 * Connect - staticCache
4 * Copyright(c) 2011 Sencha Inc.
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var utils = require('../utils')
13 , Cache = require('../cache')
14 , fresh = require('fresh');
15
16/**
17 * Static cache:
18 *
19 * Enables a memory cache layer on top of
20 * the `static()` middleware, serving popular
21 * static files.
22 *
23 * By default a maximum of 128 objects are
24 * held in cache, with a max of 256k each,
25 * totalling ~32mb.
26 *
27 * A Least-Recently-Used (LRU) cache algo
28 * is implemented through the `Cache` object,
29 * simply rotating cache objects as they are
30 * hit. This means that increasingly popular
31 * objects maintain their positions while
32 * others get shoved out of the stack and
33 * garbage collected.
34 *
35 * Benchmarks:
36 *
37 * static(): 2700 rps
38 * node-static: 5300 rps
39 * static() + staticCache(): 7500 rps
40 *
41 * Options:
42 *
43 * - `maxObjects` max cache objects [128]
44 * - `maxLength` max cache object length 256kb
45 *
46 * @param {Object} options
47 * @return {Function}
48 * @api public
49 */
50
51module.exports = function staticCache(options){
52 var options = options || {}
53 , cache = new Cache(options.maxObjects || 128)
54 , maxlen = options.maxLength || 1024 * 256;
55
56 console.warn('connect.staticCache() is deprecated and will be removed in 3.0');
57 console.warn('use varnish or similar reverse proxy caches.');
58
59 return function staticCache(req, res, next){
60 var key = cacheKey(req)
61 , ranges = req.headers.range
62 , hasCookies = req.headers.cookie
63 , hit = cache.get(key);
64
65 // cache static
66 // TODO: change from staticCache() -> cache()
67 // and make this work for any request
68 req.on('static', function(stream){
69 var headers = res._headers
70 , cc = utils.parseCacheControl(headers['cache-control'] || '')
71 , contentLength = headers['content-length']
72 , hit;
73
74 // dont cache set-cookie responses
75 if (headers['set-cookie']) return hasCookies = true;
76
77 // dont cache when cookies are present
78 if (hasCookies) return;
79
80 // ignore larger files
81 if (!contentLength || contentLength > maxlen) return;
82
83 // don't cache partial files
84 if (headers['content-range']) return;
85
86 // dont cache items we shouldn't be
87 // TODO: real support for must-revalidate / no-cache
88 if ( cc['no-cache']
89 || cc['no-store']
90 || cc['private']
91 || cc['must-revalidate']) return;
92
93 // if already in cache then validate
94 if (hit = cache.get(key)){
95 if (headers.etag == hit[0].etag) {
96 hit[0].date = new Date;
97 return;
98 } else {
99 cache.remove(key);
100 }
101 }
102
103 // validation notifiactions don't contain a steam
104 if (null == stream) return;
105
106 // add the cache object
107 var arr = [];
108
109 // store the chunks
110 stream.on('data', function(chunk){
111 arr.push(chunk);
112 });
113
114 // flag it as complete
115 stream.on('end', function(){
116 var cacheEntry = cache.add(key);
117 delete headers['x-cache']; // Clean up (TODO: others)
118 cacheEntry.push(200);
119 cacheEntry.push(headers);
120 cacheEntry.push.apply(cacheEntry, arr);
121 });
122 });
123
124 if (req.method == 'GET' || req.method == 'HEAD') {
125 if (ranges) {
126 next();
127 } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
128 res.setHeader('X-Cache', 'HIT');
129 respondFromCache(req, res, hit);
130 } else {
131 res.setHeader('X-Cache', 'MISS');
132 next();
133 }
134 } else {
135 next();
136 }
137 }
138};
139
140/**
141 * Respond with the provided cached value.
142 * TODO: Assume 200 code, that's iffy.
143 *
144 * @param {Object} req
145 * @param {Object} res
146 * @param {Object} cacheEntry
147 * @return {String}
148 * @api private
149 */
150
151function respondFromCache(req, res, cacheEntry) {
152 var status = cacheEntry[0]
153 , headers = utils.merge({}, cacheEntry[1])
154 , content = cacheEntry.slice(2);
155
156 headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
157
158 switch (req.method) {
159 case 'HEAD':
160 res.writeHead(status, headers);
161 res.end();
162 break;
163 case 'GET':
164 if (utils.conditionalGET(req) && fresh(req.headers, headers)) {
165 headers['content-length'] = 0;
166 res.writeHead(304, headers);
167 res.end();
168 } else {
169 res.writeHead(status, headers);
170
171 function write() {
172 while (content.length) {
173 if (false === res.write(content.shift())) {
174 res.once('drain', write);
175 return;
176 }
177 }
178 res.end();
179 }
180
181 write();
182 }
183 break;
184 default:
185 // This should never happen.
186 res.writeHead(500, '');
187 res.end();
188 }
189}
190
191/**
192 * Determine whether or not a cached value must be revalidated.
193 *
194 * @param {Object} req
195 * @param {Object} cacheEntry
196 * @return {String}
197 * @api private
198 */
199
200function mustRevalidate(req, cacheEntry) {
201 var cacheHeaders = cacheEntry[1]
202 , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
203 , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
204 , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
205
206 if ( cacheCC['no-cache']
207 || cacheCC['must-revalidate']
208 || cacheCC['proxy-revalidate']) return true;
209
210 if (reqCC['no-cache']) return true;
211
212 if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
213
214 if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
215
216 return false;
217}
218
219/**
220 * The key to use in the cache. For now, this is the URL path and query.
221 *
222 * 'http://example.com?key=value' -> '/?key=value'
223 *
224 * @param {Object} req
225 * @return {String}
226 * @api private
227 */
228
229function cacheKey(req) {
230 return utils.parseUrl(req).path;
231}