├── .mailmap ├── .gitignore ├── .travis.yml ├── Makefile ├── package.json ├── HISTORY.md ├── README.md ├── index.js └── test └── index.js /.mailmap: -------------------------------------------------------------------------------- 1 | Michał Gołębiowski-Owczarek 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store* 2 | ehthumbs.db 3 | Thumbs.db 4 | node_modules 5 | npm-debug.log 6 | coverage 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | - "14" 7 | script: "make test-travis" 8 | after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @NODE_ENV=test ./node_modules/.bin/mocha \ 3 | --require should \ 4 | --require should-http \ 5 | --harmony \ 6 | --reporter spec \ 7 | --bail 8 | 9 | test-cov: 10 | @NODE_ENV=test node --harmony \ 11 | node_modules/.bin/istanbul cover \ 12 | ./node_modules/.bin/_mocha \ 13 | -- -u exports \ 14 | --require should \ 15 | --require should-http \ 16 | --reporter spec \ 17 | --bail 18 | 19 | test-travis: 20 | @NODE_ENV=test node --harmony \ 21 | node_modules/.bin/istanbul cover \ 22 | ./node_modules/.bin/_mocha \ 23 | --report lcovonly \ 24 | -- -u exports \ 25 | --require should \ 26 | --require should-http \ 27 | --reporter spec \ 28 | --bail 29 | 30 | clean: 31 | @rm -rf node_modules 32 | 33 | .PHONY: test clean 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-static-cache", 3 | "description": "Static cache for koa", 4 | "version": "5.1.4", 5 | "author": { 6 | "name": "Jonathan Ong", 7 | "email": "me@jongleberry.com", 8 | "url": "http://jongleberry.com", 9 | "twitter": "https://twitter.com/jongleberry" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Jeremiah Senkpiel", 14 | "email": "fishrock123@rocketmail.com", 15 | "url": "https://searchbeam.jit.su", 16 | "twitter": "https://twitter.com/fishrock123" 17 | }, 18 | { 19 | "name": "dead_horse", 20 | "email": "dead_horse@qq.com", 21 | "url": "http://deadhorse.me", 22 | "twitter": "https://twitter.com/deadhorse_busi" 23 | } 24 | ], 25 | "files": [ 26 | "index.js" 27 | ], 28 | "keywords": [ 29 | "koa", 30 | "middleware", 31 | "file", 32 | "static", 33 | "cache", 34 | "gzip", 35 | "sendfile" 36 | ], 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/koajs/static-cache.git" 41 | }, 42 | "bugs": { 43 | "mail": "me@jongleberry.com", 44 | "url": "https://github.com/koajs/static-cache/issues" 45 | }, 46 | "dependencies": { 47 | "compressible": "^2.0.6", 48 | "debug": "^3.1.0", 49 | "fs-readdir-recursive": "^1.0.0", 50 | "mime-types": "^2.1.8", 51 | "mz": "^2.7.0" 52 | }, 53 | "devDependencies": { 54 | "bluebird": "3", 55 | "istanbul": "~0.4.1", 56 | "koa": "2", 57 | "mocha": "2", 58 | "should": "8", 59 | "should-http": "0.0.4", 60 | "supertest": "1", 61 | "ylru": "1" 62 | }, 63 | "scripts": { 64 | "test": "make test" 65 | }, 66 | "engines": { 67 | "node": ">= 7.6.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2 | 5.1.4 / 2020-08-03 3 | ================== 4 | 5 | **fixes** 6 | * [[`2735fab`](http://github.com/koajs/static-cache/commit/2735fab6b9303da8ea941eab66f719440e0f2763)] - fix: remove unused require (dead-horse <>) 7 | * [[`e12d920`](http://github.com/koajs/static-cache/commit/e12d9202e4900f17f7808e505ae39c161615be7f)] - fix: correct the time compare (dead-horse <>) 8 | * [[`c24e6c3`](http://github.com/koajs/static-cache/commit/c24e6c3fe59b896b92a69045deaa0766f6c40836)] - fix: mtime (#93) (Jlin <<565774991@qq.com>>) 9 | 10 | 5.1.3 / 2020-04-29 11 | ================== 12 | 13 | **fixes** 14 | * [[`dca1edf`](http://github.com/koajs/static-cache/commit/dca1edf0b641993921b33c0289dbd73ff4bc1c13)] - fix: alias should work when preload = false (#84) (TZ | 天猪 <>) 15 | 16 | 5.1.2 / 2018-02-06 17 | ================== 18 | 19 | **others** 20 | * [[`ba60f3d`](http://github.com/koajs/static-cache/commit/ba60f3d859e98efd41a625fd0410ff4d930bf37b)] - deps: use ^ (dead-horse <>) 21 | * [[`82274d0`](http://github.com/koajs/static-cache/commit/82274d02d3601282cb363a2339081393ef2bf83d)] - deps: Update package.json bump debug and mz versions (#73) (Iain Maitland <>) 22 | 23 | 5.1.1 / 2017-06-13 24 | ================== 25 | 26 | * fix: only load file under options.dir (#67) 27 | 28 | 5.1.0 / 2017-06-01 29 | ================== 30 | 31 | * feat: files support lru (#64) 32 | * Update mz (#61) 33 | 34 | 5.0.1 / 2017-04-19 35 | ================== 36 | 37 | * Add node.js v7.6.0 support (#58) 38 | 39 | 5.0.0 / 2017-04-01 40 | ================== 41 | 42 | * Support Koa 2 (#57) 43 | 44 | 4.0.0 / 2017-02-21 45 | ================== 46 | 47 | * refactor: check prefix first to avoid calculate (#56) 48 | 49 | 3.2.0 / 2017-01-07 50 | ================== 51 | 52 | * feat: support options.preload (#55) 53 | 54 | 3.1.7 / 2016-04-07 55 | ================== 56 | 57 | * update mz to 2.4.0 58 | 59 | 3.1.6 / 2016-03-22 60 | ================== 61 | 62 | * fix: don't catch downstream error 63 | 64 | 3.1.5 / 2016-03-02 65 | ================== 66 | 67 | * fix: dynamic load file bug on windows platform 68 | 69 | 3.1.4 / 2016-01-04 70 | ================== 71 | 72 | * bump deps 73 | * use mz 74 | 75 | 3.1.3 / 2015-11-26 76 | ================== 77 | 78 | * Fix broken mtime comparison 79 | 80 | 3.1.2 / 2015-07-08 81 | ================== 82 | 83 | * bugfix: error on dynamic files 84 | 85 | 3.1.1 / 2015-04-17 86 | ================== 87 | 88 | * fix: options.prefix bug in windows, fixes #39 89 | 90 | 3.1.0 / 2015-03-28 91 | ================== 92 | 93 | * Merge pull request #33 from AlexeyKhristov/gz 94 | 95 | 3.0.3 / 2015-03-28 96 | ================== 97 | 98 | * fix problem, cache is not used in dynamic mode 99 | 100 | 3.0.2 / 2015-03-18 101 | ================== 102 | 103 | * fix options.prefix bug in windows, fixes #36 104 | 105 | 3.0.1 / 2015-01-06 106 | ================== 107 | 108 | * feat(dynamic): add dynamic option to support dynamic load 109 | * fix(dynamic): use stat to detect folder 110 | 111 | 3.0.0 / 2015-01-06 112 | ================== 113 | 114 | * fix(test): typo 115 | * fix(buffer): keep the old logic of treat unbuffered file 116 | * feat: add opt.buffer false to serve file not cache at all 117 | * fix: support load file dynamic, close #30 118 | 119 | 2.0.2 / 2015-01-05 120 | ================== 121 | 122 | * fix normalize bug in windows, fixes #29 123 | 124 | 2.0.1 / 2014-12-02 125 | ================== 126 | 127 | * accept abnormal path, like: //index.html 128 | 129 | 2.0.0 / 2014-11-14 130 | ================== 131 | 132 | * bump koa 133 | * only response GET and HEAD 134 | 135 | 1.2.0 / 2014-09-18 136 | ================== 137 | 138 | * bump compressible and mime-types 139 | * decodeURI when use this.path as key to fetch value from files object 140 | 141 | 1.1.0 / 2014-07-16 142 | ================== 143 | 144 | * replace mime by mime-types 145 | * remove onerror and destroy, let koa hanlde these stuff 146 | 147 | 1.0.10 / 2014-05-18 148 | ================== 149 | 150 | * bump fs-readdir-recursive, fixed #14 151 | * fix bad argument handling, fixed #20 152 | * should not return gzip buffer when accept encoding not include gzip 153 | 154 | 1.0.9 / 2014-03-31 155 | ================== 156 | 157 | * add url prefix option 158 | 159 | 1.0.8 / 2014-03-31 160 | ================== 161 | 162 | * support options.dir, default to process.cwd() 163 | * add vary, check file's length when gzip 164 | * Ensure files can be gzipped via compressible. 165 | 166 | 1.0.7 / 2014-03-26 167 | ================== 168 | 169 | * add options.gzip to control gzip, support stream's gzip 170 | * add gzip support for buffers 171 | 172 | 1.0.3 / 2014-01-14 173 | ================== 174 | 175 | * update `on-socket-error` 176 | 177 | 1.0.0 / 2013-12-21 178 | ================== 179 | 180 | * use `yield* next` 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa Static Cache 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![David deps][david-image]][david-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/koa-static-cache.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/koa-static-cache 10 | [travis-image]: https://img.shields.io/travis/koajs/static-cache.svg?style=flat-square 11 | [travis-url]: https://travis-ci.org/koajs/static-cache 12 | [coveralls-image]: https://img.shields.io/coveralls/koajs/static-cache.svg?style=flat-square 13 | [coveralls-url]: https://coveralls.io/r/koajs/static-cache?branch=master 14 | [david-image]: https://img.shields.io/david/koajs/static-cache.svg?style=flat-square 15 | [david-url]: https://david-dm.org/koajs/static-cache 16 | 17 | Static server for koa. 18 | 19 | Differences between this library and other libraries such as [static](https://github.com/koajs/static): 20 | 21 | - There is no directory or `index.html` support. 22 | - You may optionally store the data in memory - it streams by default. 23 | - Caches the assets on initialization - you need to restart the process to update the assets.(can turn off with options.preload = false) 24 | - Uses MD5 hash sum as an ETag. 25 | - Uses .gz files if present on disk, like nginx gzip_static module 26 | 27 | ## Installation 28 | 29 | ```js 30 | $ npm install koa-static-cache 31 | ``` 32 | 33 | ## API 34 | 35 | ### staticCache(dir [, options] [, files]) 36 | 37 | ```js 38 | var path = require('path') 39 | var staticCache = require('koa-static-cache') 40 | 41 | app.use(staticCache(path.join(__dirname, 'public'), { 42 | maxAge: 365 * 24 * 60 * 60 43 | })) 44 | ``` 45 | 46 | - `dir` (str) - the directory you wish to serve, priority than `options.dir`. 47 | - `options.dir` (str) - the directory you wish to serve, default to `process.cwd`. 48 | - `options.maxAge` (int) - cache control max age for the files, `0` by default. 49 | - `options.cacheControl` (str) - optional cache control header. Overrides `options.maxAge`. 50 | - `options.buffer` (bool) - store the files in memory instead of streaming from the filesystem on each request. 51 | - `options.gzip` (bool) - when request's accept-encoding include gzip, files will compressed by gzip. 52 | - `options.usePrecompiledGzip` (bool) - try use gzip files, loaded from disk, like nginx gzip_static 53 | - `options.alias` (obj) - object map of aliases. See below. 54 | - `options.prefix` (str) - the url prefix you wish to add, default to `''`. 55 | - `options.dynamic` (bool) - dynamic load file which not cached on initialization. 56 | - `options.filter` (function | array) - filter files at init dir, for example - skip non build (source) files. If array set - allow only listed files 57 | - `options.preload` (bool) - caches the assets on initialization or not, default to `true`. always work together with `options.dynamic`. 58 | - `options.files` (obj) - optional files object. See below. 59 | - `files` (obj) - optional files object. See below. 60 | ### Aliases 61 | 62 | For example, if you have this alias object: 63 | 64 | ```js 65 | { 66 | '/favicon.png': '/favicon-32.png' 67 | } 68 | ``` 69 | 70 | Then requests to `/favicon.png` will actually return `/favicon-32.png` without redirects or anything. 71 | This is particularly important when serving [favicons](https://github.com/audreyr/favicon-cheat-sheet) as you don't want to store duplicate images. 72 | 73 | ### Files 74 | 75 | You can pass in an optional files object. 76 | This allows you to do two things: 77 | 78 | #### Combining directories into a single middleware 79 | 80 | Instead of doing: 81 | 82 | ```js 83 | app.use(staticCache('/public/js')) 84 | app.use(staticCache('/public/css')) 85 | ``` 86 | 87 | You can do this: 88 | 89 | ```js 90 | var files = {} 91 | 92 | // Mount the middleware 93 | app.use(staticCache('/public/js', {}, files)) 94 | 95 | // Add additional files 96 | staticCache('/public/css', {}, files) 97 | ``` 98 | 99 | The benefit is that you'll have one less function added to the stack as well as doing one hash lookup instead of two. 100 | 101 | #### Editing the files object 102 | 103 | For example, if you want to change the max age of `/package.json`, you can do the following: 104 | 105 | ```js 106 | var files = {} 107 | 108 | app.use(staticCache('/public', { 109 | maxAge: 60 * 60 * 24 * 365 110 | }, files)) 111 | 112 | files['/package.json'].maxAge = 60 * 60 * 24 * 30 113 | ``` 114 | 115 | #### Using a LRU cache to avoid OOM when dynamic mode enabled 116 | 117 | You can pass in a lru cache instance which has tow methods: `get(key)` and `set(key, value)`. 118 | 119 | ```js 120 | var LRU = require('lru-cache') 121 | var files = new LRU({ max: 1000 }) 122 | 123 | app.use(staticCache({ 124 | dir: '/public', 125 | dynamic: true, 126 | files: files 127 | })) 128 | ``` 129 | 130 | ## License 131 | 132 | The MIT License (MIT) 133 | 134 | Copyright (c) 2013 Jonathan Ong me@jongleberry.com 135 | 136 | Permission is hereby granted, free of charge, to any person obtaining a copy 137 | of this software and associated documentation files (the "Software"), to deal 138 | in the Software without restriction, including without limitation the rights 139 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 140 | copies of the Software, and to permit persons to whom the Software is 141 | furnished to do so, subject to the following conditions: 142 | 143 | The above copyright notice and this permission notice shall be included in 144 | all copies or substantial portions of the Software. 145 | 146 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 147 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 148 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 149 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 150 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 151 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 152 | THE SOFTWARE. 153 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | var fs = require('mz/fs') 3 | var zlib = require('mz/zlib') 4 | var path = require('path') 5 | var mime = require('mime-types') 6 | var compressible = require('compressible') 7 | var readDir = require('fs-readdir-recursive') 8 | var debug = require('debug')('koa-static-cache') 9 | 10 | module.exports = function staticCache(dir, options, files) { 11 | if (typeof dir === 'object') { 12 | files = options 13 | options = dir 14 | dir = null 15 | } 16 | 17 | options = options || {} 18 | // prefix must be ASCII code 19 | options.prefix = (options.prefix || '').replace(/\/*$/, '/') 20 | files = new FileManager(files || options.files) 21 | dir = dir || options.dir || process.cwd() 22 | dir = path.normalize(dir) 23 | var enableGzip = !!options.gzip 24 | var filePrefix = path.normalize(options.prefix.replace(/^\//, '')) 25 | 26 | // option.filter 27 | var fileFilter = function () { return true } 28 | if (Array.isArray(options.filter)) fileFilter = function (file) { return ~options.filter.indexOf(file) } 29 | if (typeof options.filter === 'function') fileFilter = options.filter 30 | 31 | if (options.preload !== false) { 32 | readDir(dir).filter(fileFilter).forEach(function (name) { 33 | loadFile(name, dir, options, files) 34 | }) 35 | } 36 | 37 | return async (ctx, next) => { 38 | // only accept HEAD and GET 39 | if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return await next() 40 | // check prefix first to avoid calculate 41 | if (ctx.path.indexOf(options.prefix) !== 0) return await next() 42 | 43 | // decode for `/%E4%B8%AD%E6%96%87` 44 | // normalize for `//index` 45 | var filename = path.normalize(safeDecodeURIComponent(ctx.path)) 46 | 47 | // check alias 48 | if (options.alias && options.alias[filename]) filename = options.alias[filename]; 49 | 50 | var file = files.get(filename) 51 | // try to load file 52 | if (!file) { 53 | if (!options.dynamic) return await next() 54 | if (path.basename(filename)[0] === '.') return await next() 55 | if (filename.charAt(0) === path.sep) filename = filename.slice(1) 56 | 57 | // trim prefix 58 | if (options.prefix !== '/') { 59 | if (filename.indexOf(filePrefix) !== 0) return await next() 60 | filename = filename.slice(filePrefix.length) 61 | } 62 | 63 | var fullpath = path.join(dir, filename) 64 | // files that can be accessd should be under options.dir 65 | if (fullpath.indexOf(dir) !== 0) { 66 | return await next() 67 | } 68 | 69 | var s 70 | try { 71 | s = await fs.stat(fullpath) 72 | } catch (err) { 73 | return await next() 74 | } 75 | if (!s.isFile()) return await next() 76 | 77 | file = loadFile(filename, dir, options, files) 78 | } 79 | 80 | ctx.status = 200 81 | 82 | if (enableGzip) ctx.vary('Accept-Encoding') 83 | 84 | if (!file.buffer) { 85 | var stats = await fs.stat(file.path) 86 | if (stats.mtime.getTime() !== file.mtime.getTime()) { 87 | file.mtime = stats.mtime 88 | file.md5 = null 89 | file.length = stats.size 90 | } 91 | } 92 | 93 | ctx.response.lastModified = file.mtime 94 | if (file.md5) ctx.response.etag = file.md5 95 | 96 | if (ctx.fresh) return ctx.status = 304 97 | 98 | ctx.type = file.type 99 | ctx.length = file.zipBuffer ? file.zipBuffer.length : file.length 100 | ctx.set('cache-control', file.cacheControl || 'public, max-age=' + file.maxAge) 101 | if (file.md5) ctx.set('content-md5', file.md5) 102 | 103 | if (ctx.method === 'HEAD') return 104 | 105 | var acceptGzip = ctx.acceptsEncodings('gzip') === 'gzip' 106 | 107 | if (file.zipBuffer) { 108 | if (acceptGzip) { 109 | ctx.set('content-encoding', 'gzip') 110 | ctx.body = file.zipBuffer 111 | } else { 112 | ctx.body = file.buffer 113 | } 114 | return 115 | } 116 | 117 | var shouldGzip = enableGzip 118 | && file.length > 1024 119 | && acceptGzip 120 | && compressible(file.type) 121 | 122 | if (file.buffer) { 123 | if (shouldGzip) { 124 | 125 | var gzFile = files.get(filename + '.gz') 126 | if (options.usePrecompiledGzip && gzFile && gzFile.buffer) { // if .gz file already read from disk 127 | file.zipBuffer = gzFile.buffer 128 | } else { 129 | file.zipBuffer = await zlib.gzip(file.buffer) 130 | } 131 | ctx.set('content-encoding', 'gzip') 132 | ctx.body = file.zipBuffer 133 | } else { 134 | ctx.body = file.buffer 135 | } 136 | return 137 | } 138 | 139 | var stream = fs.createReadStream(file.path) 140 | 141 | // update file hash 142 | if (!file.md5) { 143 | var hash = crypto.createHash('md5') 144 | stream.on('data', hash.update.bind(hash)) 145 | stream.on('end', function () { 146 | file.md5 = hash.digest('base64') 147 | }) 148 | } 149 | 150 | ctx.body = stream 151 | // enable gzip will remove content length 152 | if (shouldGzip) { 153 | ctx.remove('content-length') 154 | ctx.set('content-encoding', 'gzip') 155 | ctx.body = stream.pipe(zlib.createGzip()) 156 | } 157 | } 158 | } 159 | 160 | function safeDecodeURIComponent(text) { 161 | try { 162 | return decodeURIComponent(text) 163 | } catch (e) { 164 | return text 165 | } 166 | } 167 | 168 | /** 169 | * load file and add file content to cache 170 | * 171 | * @param {String} name 172 | * @param {String} dir 173 | * @param {Object} options 174 | * @param {Object} files 175 | * @return {Object} 176 | * @api private 177 | */ 178 | 179 | function loadFile(name, dir, options, files) { 180 | var pathname = path.normalize(path.join(options.prefix, name)) 181 | if (!files.get(pathname)) files.set(pathname, {}) 182 | var obj = files.get(pathname) 183 | var filename = obj.path = path.join(dir, name) 184 | var stats = fs.statSync(filename) 185 | var buffer = fs.readFileSync(filename) 186 | 187 | obj.cacheControl = options.cacheControl 188 | obj.maxAge = (typeof obj.maxAge === 'number' ? obj.maxAge : options.maxAge) || 0 189 | obj.type = obj.mime = mime.lookup(pathname) || 'application/octet-stream' 190 | obj.mtime = stats.mtime 191 | obj.length = stats.size 192 | obj.md5 = crypto.createHash('md5').update(buffer).digest('base64') 193 | 194 | debug('file: ' + JSON.stringify(obj, null, 2)) 195 | if (options.buffer) 196 | obj.buffer = buffer 197 | 198 | buffer = null 199 | return obj 200 | } 201 | 202 | function FileManager(store) { 203 | if (store && typeof store.set === 'function' && typeof store.get === 'function') { 204 | this.store = store 205 | } else { 206 | this.map = store || Object.create(null) 207 | } 208 | } 209 | 210 | FileManager.prototype.get = function (key) { 211 | return this.store ? this.store.get(key) : this.map[key] 212 | } 213 | 214 | FileManager.prototype.set = function (key, value) { 215 | if (this.store) return this.store.set(key, value) 216 | this.map[key] = value 217 | } 218 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var crypto = require('crypto') 3 | var zlib = require('zlib') 4 | var request = require('supertest') 5 | var should = require('should') 6 | var Koa = require('koa') 7 | var http = require('http') 8 | var path = require('path') 9 | var staticCache = require('..') 10 | var LRU = require('ylru') 11 | 12 | var app = new Koa() 13 | var files = {} 14 | app.use(staticCache(path.join(__dirname, '..'), { 15 | alias: { 16 | '/package': '/package.json' 17 | }, 18 | filter(file) { 19 | return !file.includes('node_modules') 20 | } 21 | }, files)) 22 | 23 | var server = http.createServer(app.callback()) 24 | 25 | var app2 = new Koa() 26 | app2.use(staticCache(path.join(__dirname, '..'), { 27 | buffer: true, 28 | filter(file) { 29 | return !file.includes('node_modules') 30 | } 31 | })) 32 | var server2 = http.createServer(app2.callback()) 33 | 34 | var app3 = new Koa() 35 | app3.use(staticCache(path.join(__dirname, '..'), { 36 | buffer: true, 37 | gzip: true, 38 | filter(file) { 39 | return !file.includes('node_modules') 40 | } 41 | })) 42 | var server3 = http.createServer(app3.callback()) 43 | 44 | var app4 = new Koa() 45 | var files4 = {} 46 | app4.use(staticCache(path.join(__dirname, '..'), { 47 | gzip: true, 48 | filter(file) { 49 | return !file.includes('node_modules') 50 | } 51 | }, files4)) 52 | 53 | var server4 = http.createServer(app4.callback()) 54 | 55 | var app5 = new Koa() 56 | app5.use(staticCache({ 57 | buffer: true, 58 | prefix: '/static', 59 | dir: path.join(__dirname, '..'), 60 | filter(file) { 61 | return !file.includes('node_modules') 62 | } 63 | })) 64 | var server5 = http.createServer(app5.callback()) 65 | 66 | var app6 = new Koa() 67 | var server6 = http.createServer(app6.callback()) 68 | 69 | describe('Static Cache', function () { 70 | 71 | it('should dir priority than options.dir', function (done) { 72 | var app = new Koa() 73 | app.use(staticCache(path.join(__dirname, '..'), { 74 | dir: __dirname 75 | })) 76 | var server = app.listen() 77 | request(server) 78 | .get('/index.js') 79 | .expect(200, done) 80 | }) 81 | 82 | it('should default options.dir works fine', function (done) { 83 | var app = new Koa() 84 | app.use(staticCache({ 85 | dir: path.join(__dirname, '..') 86 | })) 87 | var server = app.listen() 88 | request(server) 89 | .get('/index.js') 90 | .expect(200, done) 91 | }) 92 | 93 | it('should accept abnormal path', function (done) { 94 | var app = new Koa() 95 | app.use(staticCache({ 96 | dir: path.join(__dirname, '..') 97 | })) 98 | var server = app.listen() 99 | request(server) 100 | .get('//index.js') 101 | .expect(200, done) 102 | }) 103 | 104 | it('should default process.cwd() works fine', function (done) { 105 | var app = new Koa() 106 | app.use(staticCache()) 107 | var server = app.listen() 108 | request(server) 109 | .get('/index.js') 110 | .expect(200, done) 111 | }) 112 | 113 | var etag 114 | it('should serve files', function (done) { 115 | request(server) 116 | .get('/index.js') 117 | .expect(200) 118 | .expect('Cache-Control', 'public, max-age=0') 119 | .expect('Content-Type', /javascript/) 120 | .end(function (err, res) { 121 | if (err) 122 | return done(err) 123 | 124 | res.should.have.header('Content-Length') 125 | res.should.have.header('Last-Modified') 126 | res.should.have.header('ETag') 127 | etag = res.headers.etag 128 | 129 | done() 130 | }) 131 | }) 132 | 133 | it('should serve files as buffers', function (done) { 134 | request(server2) 135 | .get('/index.js') 136 | .expect(200) 137 | .expect('Cache-Control', 'public, max-age=0') 138 | .expect('Content-Type', /javascript/) 139 | .end(function (err, res) { 140 | if (err) 141 | return done(err) 142 | 143 | res.should.have.header('Content-Length') 144 | res.should.have.header('Last-Modified') 145 | res.should.have.header('ETag') 146 | 147 | etag = res.headers.etag 148 | 149 | done() 150 | }) 151 | }) 152 | 153 | it('should serve recursive files', function (done) { 154 | request(server) 155 | .get('/test/index.js') 156 | .expect(200) 157 | .expect('Cache-Control', 'public, max-age=0') 158 | .expect('Content-Type', /javascript/) 159 | .end(function (err, res) { 160 | if (err) 161 | return done(err) 162 | 163 | res.should.have.header('Content-Length') 164 | res.should.have.header('Last-Modified') 165 | res.should.have.header('ETag') 166 | 167 | done() 168 | }) 169 | }) 170 | 171 | it('should not serve hidden files', function (done) { 172 | request(server) 173 | .get('/.gitignore') 174 | .expect(404, done) 175 | }) 176 | 177 | it('should support conditional HEAD requests', function (done) { 178 | request(server) 179 | .head('/index.js') 180 | .set('If-None-Match', etag) 181 | .expect(304, done) 182 | }) 183 | 184 | it('should support conditional GET requests', function (done) { 185 | request(server) 186 | .get('/index.js') 187 | .set('If-None-Match', etag) 188 | .expect(304, done) 189 | }) 190 | 191 | it('should support HEAD', function (done) { 192 | request(server) 193 | .head('/index.js') 194 | .expect(200) 195 | .expect('', done) 196 | }) 197 | 198 | it('should support 404 Not Found for other Methods to allow downstream', 199 | function (done) { 200 | request(server) 201 | .put('/index.js') 202 | .expect(404, done) 203 | }) 204 | 205 | it('should ignore query strings', function (done) { 206 | request(server) 207 | .get('/index.js?query=string') 208 | .expect(200, done) 209 | }) 210 | 211 | it('should alias paths', function (done) { 212 | request(server) 213 | .get('/package') 214 | .expect('Content-Type', /json/) 215 | .expect(200, done) 216 | }) 217 | 218 | it('should be configurable via object', function (done) { 219 | files['/package.json'].maxAge = 1 220 | 221 | request(server) 222 | .get('/package.json') 223 | .expect('Cache-Control', 'public, max-age=1') 224 | .expect(200, done) 225 | }) 226 | 227 | it('should set the maxAge 0', function (done) { 228 | app6.use(staticCache(path.join(__dirname, '..'), { 229 | maxAge: 365 * 24 * 60 * 60 230 | }, { 231 | '/package.json': { 232 | maxAge: 0 233 | } 234 | })) 235 | 236 | request(server6) 237 | .get('/package.json') 238 | .expect('Cache-Control', 'public, max-age=0') 239 | .expect(200, done) 240 | }) 241 | 242 | it('should set the etag and content-md5 headers', function (done) { 243 | var pk = fs.readFileSync('package.json') 244 | var md5 = crypto.createHash('md5').update(pk).digest('base64') 245 | 246 | request(server) 247 | .get('/package.json') 248 | .expect('ETag', '"' + md5 + '"') 249 | .expect('Content-MD5', md5) 250 | .expect(200, done) 251 | }) 252 | 253 | it('should set Last-Modified if file modified and not buffered', function (done) { 254 | setTimeout(function () { 255 | var readme = fs.readFileSync('README.md', 'utf8') 256 | fs.writeFileSync('README.md', readme, 'utf8') 257 | var mtime = fs.statSync('README.md').mtime 258 | var md5 = files['/README.md'].md5 259 | request(server) 260 | .get('/README.md') 261 | .expect(200, function (err, res) { 262 | res.should.have.header('Content-Length') 263 | res.should.have.header('Last-Modified') 264 | res.should.not.have.header('ETag') 265 | files['/README.md'].mtime.should.eql(mtime) 266 | setTimeout(function () { 267 | files['/README.md'].md5.should.equal(md5) 268 | }, 10) 269 | done() 270 | }) 271 | }, 1000) 272 | }) 273 | 274 | it('should set Last-Modified if file rollback and not buffered', function (done) { 275 | setTimeout(function () { 276 | var readme = fs.readFileSync('README.md', 'utf8') 277 | fs.writeFileSync('README.md', readme, 'utf8') 278 | var mtime = fs.statSync('README.md').mtime 279 | var md5 = files['/README.md'].md5 280 | request(server) 281 | .get('/README.md') 282 | .expect(200, function (err, res) { 283 | res.should.have.header('Content-Length') 284 | res.should.have.header('Last-Modified') 285 | res.should.not.have.header('ETag') 286 | files['/README.md'].mtime.should.eql(mtime) 287 | setTimeout(function () { 288 | files['/README.md'].md5.should.equal(md5) 289 | }, 10) 290 | done() 291 | }) 292 | }, 1000) 293 | }) 294 | 295 | it('should serve files with gzip buffer', function (done) { 296 | var index = fs.readFileSync('index.js') 297 | zlib.gzip(index, function (err, content) { 298 | request(server3) 299 | .get('/index.js') 300 | .set('Accept-Encoding', 'gzip') 301 | .expect(200) 302 | .expect('Cache-Control', 'public, max-age=0') 303 | .expect('Content-Encoding', 'gzip') 304 | .expect('Content-Type', /javascript/) 305 | .expect('Content-Length', content.length) 306 | .expect('Vary', 'Accept-Encoding') 307 | .expect(index.toString()) 308 | .end(function (err, res) { 309 | if (err) 310 | return done(err) 311 | res.should.have.header('Content-Length') 312 | res.should.have.header('Last-Modified') 313 | res.should.have.header('ETag') 314 | 315 | etag = res.headers.etag 316 | 317 | done() 318 | }) 319 | }) 320 | }) 321 | 322 | it('should not serve files with gzip buffer when accept encoding not include gzip', 323 | function (done) { 324 | var index = fs.readFileSync('index.js') 325 | request(server3) 326 | .get('/index.js') 327 | .set('Accept-Encoding', '') 328 | .expect(200) 329 | .expect('Cache-Control', 'public, max-age=0') 330 | .expect('Content-Type', /javascript/) 331 | .expect('Content-Length', index.length) 332 | .expect('Vary', 'Accept-Encoding') 333 | .expect(index.toString()) 334 | .end(function (err, res) { 335 | if (err) 336 | return done(err) 337 | res.should.not.have.header('Content-Encoding') 338 | res.should.have.header('Content-Length') 339 | res.should.have.header('Last-Modified') 340 | res.should.have.header('ETag') 341 | done() 342 | }) 343 | }) 344 | 345 | it('should serve files with gzip stream', function (done) { 346 | var index = fs.readFileSync('index.js') 347 | zlib.gzip(index, function (err, content) { 348 | request(server4) 349 | .get('/index.js') 350 | .set('Accept-Encoding', 'gzip') 351 | .expect(200) 352 | .expect('Cache-Control', 'public, max-age=0') 353 | .expect('Content-Encoding', 'gzip') 354 | .expect('Content-Type', /javascript/) 355 | .expect('Vary', 'Accept-Encoding') 356 | .expect(index.toString()) 357 | .end(function (err, res) { 358 | if (err) 359 | return done(err) 360 | res.should.not.have.header('Content-Length') 361 | res.should.have.header('Last-Modified') 362 | res.should.have.header('ETag') 363 | 364 | etag = res.headers.etag 365 | 366 | done() 367 | }) 368 | }) 369 | }) 370 | 371 | it('should serve files with prefix', function (done) { 372 | request(server5) 373 | .get('/static/index.js') 374 | .expect(200) 375 | .expect('Cache-Control', 'public, max-age=0') 376 | .expect('Content-Type', /javascript/) 377 | .end(function (err, res) { 378 | if (err) 379 | return done(err) 380 | 381 | res.should.have.header('Content-Length') 382 | res.should.have.header('Last-Modified') 383 | res.should.have.header('ETag') 384 | 385 | etag = res.headers.etag 386 | 387 | done() 388 | }) 389 | }) 390 | 391 | it('should 404 when dynamic = false', function (done) { 392 | var app = new Koa() 393 | app.use(staticCache({dynamic: false})) 394 | var server = app.listen() 395 | fs.writeFileSync('a.js', 'hello world') 396 | 397 | request(server) 398 | .get('/a.js') 399 | .expect(404, function(err) { 400 | fs.unlinkSync('a.js') 401 | done(err) 402 | }) 403 | }) 404 | 405 | it('should work fine when new file added in dynamic mode', function (done) { 406 | var app = new Koa() 407 | app.use(staticCache({dynamic: true})) 408 | var server = app.listen() 409 | fs.writeFileSync('a.js', 'hello world') 410 | 411 | request(server) 412 | .get('/a.js') 413 | .expect(200, function(err) { 414 | fs.unlinkSync('a.js') 415 | done(err) 416 | }) 417 | }) 418 | 419 | it('should work fine when new file added in dynamic and prefix mode', function (done) { 420 | var app = new Koa() 421 | app.use(staticCache({dynamic: true, prefix: '/static'})) 422 | var server = app.listen() 423 | fs.writeFileSync('a.js', 'hello world') 424 | 425 | request(server) 426 | .get('/static/a.js') 427 | .expect(200, function(err) { 428 | fs.unlinkSync('a.js') 429 | done(err) 430 | }) 431 | }) 432 | 433 | it('should work fine when new file added in dynamic mode with LRU', function (done) { 434 | var app = new Koa() 435 | var files = new LRU(1) 436 | app.use(staticCache({dynamic: true, files: files})) 437 | var server = app.listen() 438 | fs.writeFileSync('a.js', 'hello world a') 439 | fs.writeFileSync('b.js', 'hello world b') 440 | fs.writeFileSync('c.js', 'hello world b') 441 | 442 | request(server) 443 | .get('/a.js') 444 | .expect(200, function(err) { 445 | should.exist(files.get('/a.js')) 446 | should.not.exist(err) 447 | 448 | request(server) 449 | .get('/b.js') 450 | .expect(200, function (err) { 451 | should.not.exist(files.get('/a.js')) 452 | should.exist(files.get('/b.js')) 453 | should.not.exist(err) 454 | 455 | request(server) 456 | .get('/c.js') 457 | .expect(200, function (err) { 458 | should.not.exist(files.get('/b.js')) 459 | should.exist(files.get('/c.js')) 460 | should.not.exist(err) 461 | 462 | request(server) 463 | .get('/a.js') 464 | .expect(200, function (err) { 465 | should.not.exist(files.get('/c.js')) 466 | should.exist(files.get('/a.js')) 467 | should.not.exist(err) 468 | fs.unlinkSync('a.js') 469 | fs.unlinkSync('b.js') 470 | fs.unlinkSync('c.js') 471 | done() 472 | }) 473 | }) 474 | }) 475 | }) 476 | }) 477 | 478 | it('should 404 when url without prefix in dynamic and prefix mode', function (done) { 479 | var app = new Koa() 480 | app.use(staticCache({dynamic: true, prefix: '/static'})) 481 | var server = app.listen() 482 | fs.writeFileSync('a.js', 'hello world') 483 | 484 | request(server) 485 | .get('/a.js') 486 | .expect(404, function(err) { 487 | fs.unlinkSync('a.js') 488 | done(err) 489 | }) 490 | }) 491 | 492 | it('should 404 when new hidden file added in dynamic mode', function (done) { 493 | var app = new Koa() 494 | app.use(staticCache({dynamic: true})) 495 | var server = app.listen() 496 | fs.writeFileSync('.a.js', 'hello world') 497 | 498 | request(server) 499 | .get('/.a.js') 500 | .expect(404, function(err) { 501 | fs.unlinkSync('.a.js') 502 | done(err) 503 | }) 504 | }) 505 | 506 | it('should 404 when file not exist in dynamic mode', function (done) { 507 | var app = new Koa() 508 | app.use(staticCache({dynamic: true})) 509 | var server = app.listen() 510 | request(server) 511 | .get('/a.js') 512 | .expect(404, done) 513 | }) 514 | 515 | it('should 404 when file not exist', function (done) { 516 | var app = new Koa() 517 | app.use(staticCache({dynamic: true})) 518 | var server = app.listen() 519 | request(server) 520 | .get('/a.js') 521 | .expect(404, done) 522 | }) 523 | 524 | it('should 404 when is folder in dynamic mode', function (done) { 525 | var app = new Koa() 526 | app.use(staticCache({dynamic: true})) 527 | var server = app.listen() 528 | request(server) 529 | .get('/test') 530 | .expect(404, done) 531 | }) 532 | 533 | it('should array options.filter works fine', function (done) { 534 | var app = new Koa() 535 | app.use(staticCache({ 536 | dir: path.join(__dirname, '..'), 537 | filter: ['index.js'] 538 | })) 539 | var server = app.listen() 540 | request(server) 541 | .get('/Makefile') 542 | .expect(404, done) 543 | }) 544 | 545 | it('should function options.filter works fine', function (done) { 546 | var app = new Koa() 547 | app.use(staticCache({ 548 | dir: path.join(__dirname, '..'), 549 | filter: function (file) { return file.indexOf('index.js') === 0 } 550 | })) 551 | var server = app.listen() 552 | request(server) 553 | .get('/Makefile') 554 | .expect(404, done) 555 | }) 556 | 557 | it('should options.dynamic and options.preload works fine', function (done) { 558 | var app = new Koa() 559 | var files = {} 560 | app.use(staticCache({ 561 | dir: path.join(__dirname, '..'), 562 | preload: false, 563 | dynamic: true, 564 | files: files 565 | })) 566 | files.should.eql({}) 567 | request(app.listen()) 568 | .get('/Makefile') 569 | .expect(200, function (err, res) { 570 | should.not.exist(err) 571 | files.should.have.keys('/Makefile') 572 | done() 573 | }) 574 | }) 575 | 576 | it('should options.alias and options.preload works fine', function (done) { 577 | var app = new Koa() 578 | var files = {} 579 | app.use(staticCache({ 580 | dir: path.join(__dirname, '..'), 581 | preload: false, 582 | dynamic: true, 583 | alias: { 584 | '/package': '/package.json' 585 | }, 586 | files: files 587 | })) 588 | files.should.eql({}) 589 | request(app.listen()) 590 | .get('/package') 591 | .expect(200, function (err, res) { 592 | if (err) return done(err) 593 | should.not.exist(err) 594 | files.should.have.keys('/package.json') 595 | files.should.not.have.keys('/package') 596 | 597 | request(app.listen()) 598 | .get('/package.json') 599 | .expect(200, function (err, res) { 600 | if (err) return done(err) 601 | should.not.exist(err) 602 | files.should.have.keys('/package.json') 603 | should.ok(Object.keys(files).length === 1) 604 | done() 605 | }) 606 | }) 607 | }) 608 | 609 | 610 | it('should loadFile under options.dir', function (done) { 611 | var app = new Koa() 612 | app.use(staticCache({ 613 | dir: __dirname, 614 | preload: false, 615 | dynamic: true, 616 | })) 617 | request(app.listen()) 618 | .get('/%2E%2E/package.json') 619 | .expect(404) 620 | .end(done) 621 | }) 622 | }) 623 | --------------------------------------------------------------------------------