Cache.prototype.cachingEnabled = function (req) { // Check it is not a json view var query = url.parse(req.url, true).query if (query.json && query.json !== 'false') return false // Disable cache for debug mode if (config.get('debug')) return false // if it's in the endpoint and caching is enabled var endpoint = this.getEndpoint(req) if (endpoint) { this.options.cache = typeof endpoint.page.settings.cache !== 'undefined' ? endpoint.page.settings.cache : this.enabled return this.enabled && this.options.cache } else { // Otherwise it might be in the public folder var file = url.parse(req.url).pathname return compressible(mime.lookup(file)) } }
function shouldCompress(req, res) { var type = res.getHeader('Content-Type'); if (type === undefined || !compressible(type)) { debug('%s not compressible', type); return false; } return true; }
return function* cash(next) { this.vary('Accept-Encoding') this.cashed = cashed yield* next // check for HTTP caching just in case if (!this.cash) { if (this.request.fresh) this.response.status = 304 return } // cache the response // only cache GET/HEAD 200s if (this.response.status !== 200) return if (!methods[this.request.method]) return var body = this.response.body if (!body) return // stringify JSON bodies if (isJSON(body)) body = this.response.body = JSON.stringify(body) // buffer streams if (typeof body.pipe === 'function') { // note: non-binary streams are NOT supported! body = this.response.body = Buffer.concat(yield toArray(body)) } // avoid any potential errors with middleware ordering if (this.response.get('Content-Encoding') || 'identity' !== 'identity') { throw new Error('Place koa-cache below any compression middleware.') } var fresh = this.request.fresh if (fresh) this.response.status = 304 var obj = { body: body, type: this.response.get('Content-Type') || null, lastModified: this.response.lastModified || null, etag: this.response.get('etag') || null, } if (compressible(obj.type) && this.response.length >= threshold) { obj.gzip = yield compress(body) if (!fresh && this.request.acceptsEncodings('gzip', 'identity') === 'gzip') { this.response.body = obj.gzip this.response.set('Content-Encoding', 'gzip') } } if (!this.response.get('Content-Encoding')) this.response.set('Content-Encoding', 'identity') yield set(this.cashKey, obj) }
exports.filter = function filter(req, res) { var type = res.getHeader('Content-Type') if(type === undefined || !compressible(type)) { debug('%s not compressible', type) return false } return true };
// get the file from cache if possible function* get(path) { var val = cache[path] if (val && val.compress && (yield fs.exists(val.compress.path))) return val var stats = yield fs.stat(path).catch(ignoreStatError) // we don't want to cache 404s because // the cache object will get infinitely large if (!stats || !stats.isFile()) return stats.path = path var file = cache[path] = { stats: stats, etag: '"' + (yield hash(path, algorithm)).toString(encoding) + '"', type: mime.contentType(extname(path)) || 'application/octet-stream', } if (!compressible(file.type)) return file // if we can compress this file, we create a .gz var compress = file.compress = { path: path + '.gz' } // delete old .gz files in case the file has been updated try { yield fs.unlink(compress.path) } catch (err) {} // save to a random file name first var tmp = path + '.' + random() + '.gz' yield function (done) { fs.createReadStream(path) .on('error', done) .pipe(zlib.createGzip()) .on('error', done) .pipe(fs.createWriteStream(tmp)) .on('error', done) .on('finish', done) } compress.stats = yield fs.stat(tmp).catch(ignoreStatError) // if the gzip size is larger than the original file, // don't bother gzipping if (compress.stats.size > stats.size) { delete file.compress yield fs.unlink(tmp) } else { // otherwise, rename to the correct path yield fs.rename(tmp, compress.path) } return file }
function getMimeType(filename) { var ext = path.extname(filename); if (ext === '.bin' || ext === '.terrain') { return { type : 'application/octet-stream', compress : true }; } else if (ext === '.md' || ext === '.glsl') { return { type : 'text/plain', compress : true }; } else if (ext === '.czml' || ext === '.geojson' || ext === '.json') { return { type : 'application/json', compress : true }; } else if (ext === '.js') { return { type : 'application/javascript', compress : true }; } else if (ext === '.svg') { return { type : 'image/svg+xml', compress : true }; } else if (ext === '.woff') { return { type : 'application/font-woff', compress : false }; } var mimeType = mime.lookup(filename); return {type : mimeType, compress : compressible(mimeType)}; }
Cached.prototype.end = function (data) { if (data) this.write(data) var self = this var buffer = Buffer.concat(this.data) this.headers['content-length'] = buffer.length delete this.data this.buffer = buffer this.md5 = crypto.createHash('md5').update(this.buffer).digest("hex") this.headers['etag'] = this.md5 if (!compressible(this.headers['content-type'])) { self.ended = true self.emit('end') return } zlib.gzip(buffer, function (e, compressed) { if (e) return self.emit('error', e) self.compressed = compressed self.ended = true self.emit('end') }) }
function compress () { let body = this.body this.vary('accept-encoding') if (!body || this.compress === false || this.method === 'HEAD') return if (statuses.empty[this.response.status] || this.response.get('content-encoding')) return if (!(this.compress === true || compressible(this.type))) return let encoding = this.acceptsEncodings('gzip', 'deflate', 'identity') if (!encoding) this.throw(406, 'supported encodings: gzip, deflate, identity') if (encoding === 'identity') return if (!(body instanceof Stream)) { if (typeof body === 'string') body = Buffer.from(body) else if (!Buffer.isBuffer(body)) body = Buffer.from(JSON.stringify(body)) if (body.length < threshold) return } this.set('content-encoding', encoding) this.remove('content-length') var stream = this.body = encodingMethods[encoding](options) if (body instanceof Stream) body.pipe(stream) else stream.end(body) }
return function* staticCache(next) { // only accept HEAD and GET if (this.method !== 'HEAD' && this.method !== 'GET') return yield* next; // decode for `/%E4%B8%AD%E6%96%87` // normalize for `//index` var filename = safeDecodeURIComponent(path.normalize(this.path)) var file = files[filename] // try to load file if (!file) { if (!options.dynamic) return yield* next if (path.basename(filename)[0] === '.') return yield* next if (filename.charAt(0) === path.sep) filename = filename.slice(1) try { var s = yield stat(path.join(dir, filename)) if (!s.isFile()) return yield* next } catch (err) { return yield* next } file = loadFile(filename, dir, options, files) } this.status = 200 if (enableGzip) this.vary('Accept-Encoding') if (!file.buffer) { var stats = yield stat(file.path) if (stats.mtime > new Date(file.mtime)) { file.mtime = stats.mtime.toUTCString() file.md5 = null file.length = stats.size } } this.response.lastModified = file.mtime if (file.md5) this.response.etag = file.md5 if (this.fresh) return this.status = 304 this.type = file.type this.length = file.zipBuffer ? file.zipBuffer.length : file.length this.set('Cache-Control', file.cacheControl || 'public, max-age=' + file.maxAge) if (file.md5) this.set('Content-MD5', file.md5) if (this.method === 'HEAD') return var acceptGzip = this.acceptsEncodings('gzip') === 'gzip'; if (file.zipBuffer) { if (acceptGzip) { this.set('Content-Encoding', 'gzip') this.body = file.zipBuffer } else { this.body = file.buffer } return } var shouldGzip = enableGzip && file.length > 1024 && acceptGzip && compressible(file.type) if (file.buffer) { if (shouldGzip) { var gzFile = files[filename + '.gz']; if (options.usePrecompiledGzip && gzFile && gzFile.buffer) { // if .gz file already read from disk file.zipBuffer = gzFile.buffer } else { file.zipBuffer = yield gzip(file.buffer) } this.set('Content-Encoding', 'gzip') this.body = file.zipBuffer } else { this.body = file.buffer } return } var stream = fs.createReadStream(file.path) // update file hash if (!file.md5) { var hash = crypto.createHash('md5') stream.on('data', hash.update.bind(hash)) stream.on('end', function () { file.md5 = hash.digest('base64') }) } this.body = stream // enable gzip will remove content length if (shouldGzip) { this.remove('Content-Length') this.set('Content-Encoding', 'gzip') this.body = stream.pipe(zlib.createGzip()) } }
this.server.app.use(function cache (req, res, next) { var enabled = self.cachingEnabled(req) if (!enabled) return next() debug('%s%s, cache enabled: %s', req.headers.host, req.url, enabled) // Check it's a page if (!self.getEndpoint(req)) return next() // get contentType that current endpoint requires var contentType = self.getReqContentType(req) // only cache GET requests if (req.method && req.method.toLowerCase() !== 'get') return next() // we build the filename with a hashed hex string so we can be unique // and avoid using file system reserved characters in the name var requestUrl = url.parse(req.url, true).path // get the host key that matches the request's host header const virtualHosts = config.get('virtualHosts') const host = Object.keys(virtualHosts).find(key => { return virtualHosts.hostnames.includes(req.headers.host) }) || '' var filename = crypto .createHash('sha1') .update(`${host}${requestUrl}`) .digest('hex') // allow query string param to bypass cache var query = url.parse(req.url, true).query var noCache = query.cache && query.cache.toString().toLowerCase() === 'false' // File extension for cache file var cacheExt = compressible(contentType) && help.canCompress(req.headers) ? '.' + help.canCompress(req.headers) : null var opts = { directory: { extension: mime.extension(contentType) + cacheExt } } // Compression settings var shouldCompress = compressible(contentType) ? help.canCompress(req.headers) : false // attempt to get from the cache self.cache .get(filename, opts) .then(stream => { debug('serving %s%s from cache', req.headers.host, req.url) if (noCache) { res.setHeader('X-Cache-Lookup', 'HIT') res.setHeader('X-Cache', 'MISS') return next() } var headers = { 'X-Cache-Lookup': 'HIT', 'X-Cache': 'HIT', 'Content-Type': contentType, 'Cache-Control': config.get('headers.cacheControl')[contentType] || 'public, max-age=86400' } // Add compression headers if (shouldCompress) headers['Content-Encoding'] = shouldCompress // Add extra headers stream.on('open', fd => { fs.fstat(fd, (_, stats) => { res.setHeader('Content-Length', stats.size) res.setHeader('ETag', etag(stats)) }) }) res.statusCode = 200 Object.keys(headers).map(i => res.setHeader(i, headers[i])) stream.pipe(res) }) .catch(() => { // not found in cache res.setHeader('X-Cache', 'MISS') res.setHeader('X-Cache-Lookup', 'MISS') return cacheResponse() }) /** * Writes the current response body to either the filesystem or a Redis server, * depending on the configuration settings */ function cacheResponse () { // file is expired or does not exist, wrap res.end and res.write to save to cache var _end = res.end var _write = res.write var data = [] res.write = function (chunk) { if (chunk) data.push(chunk) _write.apply(res, arguments) } res.end = function (chunk) { // respond before attempting to cache _end.apply(res, arguments) if (chunk && !data.length) data.push(chunk) // if response is not 200 don't cache if (res.statusCode !== 200) return // cache the content, with applicable file extension try { self.cache.set(filename, Buffer.concat(data), opts).then(() => {}) } catch (e) { console.log('Could not cache content: ' + requestUrl) } } return next() } })
, maybeGzip = function (contentType, buffer, callback) { if (!contentType || !compressible(contentType)) return callback(null) zlib.gzip(buffer, callback) }
exports.filter = function(req, res){ return compressible(res.getHeader('Content-Type')); };
return function* staticCache(next) { var file = files[safeDecodeURIComponent(this.path)] if (!file) return yield* next switch (this.method) { case 'HEAD': case 'GET': this.status = 200 if (enableGzip) this.vary('Accept-Encoding') if (!file.buffer) { var stats = yield stat(file.path) if (stats.mtime > new Date(file.mtime)) { file.mtime = stats.mtime.toUTCString() file.md5 = null file.length = stats.size } } this.response.lastModified = file.mtime if (file.md5) this.response.etag = file.md5 if (this.fresh) return this.status = 304 this.type = file.type this.length = file.zipBuffer ? file.zipBuffer.length : file.length this.set('Cache-Control', file.cacheControl || 'public, max-age=' + file.maxAge) if (file.md5) this.set('Content-MD5', file.md5) if (this.method === 'HEAD') return var acceptGzip = this.acceptsEncodings('gzip') === 'gzip'; if (file.zipBuffer) { if (acceptGzip) { this.set('Content-Encoding', 'gzip') this.body = file.zipBuffer } else { this.body = file.buffer } return } var shouldGzip = enableGzip && file.length > 1024 && acceptGzip && compressible(file.type) if (file.buffer) { if (shouldGzip) { file.zipBuffer = yield gzip(file.buffer) this.set('Content-Encoding', 'gzip') this.body = file.zipBuffer } else { this.body = file.buffer } return } var stream = fs.createReadStream(file.path) // update file hash if (!file.md5) { var hash = crypto.createHash('md5') stream.on('data', hash.update.bind(hash)) stream.on('end', function () { file.md5 = hash.digest('base64') }) } this.body = stream // enable gzip will remove content length if (shouldGzip) { this.remove('Content-Length') this.set('Content-Encoding', 'gzip') this.body = stream.pipe(zlib.createGzip()) } return case 'OPTIONS': this.status = 204 this.set('Allow', 'HEAD,GET,OPTIONS') return default: this.status = 405 this.set('Allow', 'HEAD,GET,OPTIONS') return } }