├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── History.md ├── LICENSE ├── Readme.md ├── example.js ├── index.js ├── package.json └── test ├── .eslintrc ├── fixtures ├── hello.txt ├── index.txt └── world │ └── index.html └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: standard 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 5 8 | versioning-strategy: increase-if-necessary 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - 14 3 | - 16 4 | - 17 5 | language: node_js 6 | script: "npm run-script test-travis" -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 5.0.0 / 2018-06-19 3 | ================== 4 | 5 | * bump deps 6 | 7 | 4.0.3 / 2018-05-17 8 | ================== 9 | 10 | * npm: disable package-lock 11 | * bump deps 12 | 13 | 4.0.2 / 2017-11-16 14 | ================== 15 | 16 | * Fix: `serve` mutates `opts` argument so it cannot be reused (#117) 17 | 18 | 4.0.1 / 2017-07-09 19 | ================== 20 | 21 | * Fix: throw error if error status not 404 22 | * fix `index: false` bug if path is directory 23 | 24 | 4.0.0 / 2017-07-03 25 | ================== 26 | 27 | * upgrade to koa-send@4 28 | * use async function 29 | 30 | 3.0.0 / 2016-03-24 31 | ================== 32 | 33 | * support koa 2.x 34 | * travis: test node@4+ 35 | 36 | 2.0.0 / 2016-01-07 37 | ================== 38 | 39 | * bump koa-send@~3.1.0 40 | 41 | 1.5.2 / 2015-11-03 42 | ================== 43 | 44 | * Fix: default index could be disabled. Closes #41 45 | 46 | 1.5.1 / 2015-10-14 47 | ================== 48 | 49 | * Fix v1.4.x → 1.5.0 broken. Closes #53 50 | 51 | 1.5.0 / 2015-10-14 52 | ================== 53 | 54 | * update koa-send@2 55 | * update devDeps 56 | 57 | 1.4.9 / 2015-02-03 58 | ================== 59 | 60 | * only support GET and HEAD requests 61 | 62 | 1.4.8 / 2014-12-17 63 | ================== 64 | 65 | * support root = `.` 66 | 67 | 1.4.7 / 2014-09-07 68 | ================== 69 | 70 | * update koa-send 71 | 72 | 1.4.5 / 2014-05-05 73 | ================== 74 | 75 | * Fix response already handled logic - Koajs now defaults status == 404. See koajs/koa#269 76 | 77 | 1.4.4 / 2014-05-04 78 | ================== 79 | 80 | * Add two missing semicolons. Closes #24 81 | * Use bash syntax highlighting for install command. Closes #23 82 | * named generator function to help debugging. Closes #20 83 | 84 | 1.4.3 / 2014-02-11 85 | ================== 86 | 87 | * update koa-send 88 | 89 | 1.4.2 / 2014-01-07 90 | ================== 91 | 92 | * update koa-send 93 | 94 | 1.4.1 / 2013-12-30 95 | ================== 96 | 97 | * fix for koa 0.2.1. Closes #12 98 | 99 | 1.4.0 / 2013-12-20 100 | ================== 101 | 102 | * add: defer option - semi-breaking change 103 | 104 | 1.3.0 / 2013-12-11 105 | ================== 106 | 107 | * refactor to use koa-send 108 | * rename maxAge -> maxage 109 | * fix: don't bother responding if response already "handled" 110 | 111 | 1.2.0 / 2013-09-14 112 | ================== 113 | 114 | * add Last-Modified. Closes #5 115 | 116 | 1.1.1 / 2013-09-13 117 | ================== 118 | 119 | * fix upstream responses 120 | 121 | 1.1.0 / 2013-09-13 122 | ================== 123 | 124 | * rewrite without send 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Koa Static Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # koa-static 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![License][license-image]][license-url] 7 | [![Downloads][downloads-image]][downloads-url] 8 | 9 | Koa static file serving middleware, wrapper for [`koa-send`](https://github.com/koajs/send). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | $ npm install koa-static 15 | ``` 16 | 17 | ## API 18 | 19 | ```js 20 | import Koa from "koa"; // CJS: require('koa'); 21 | import serve from "koa-static"; // CJS: require('koa-static') 22 | const app = new Koa(); 23 | app.use(serve(root, opts)); 24 | ``` 25 | 26 | - `root` root directory string. nothing above this root directory can be served 27 | - `opts` options object. 28 | 29 | ### Options 30 | 31 | - `maxage` Browser cache max-age in milliseconds. defaults to 0 32 | - `hidden` Allow transfer of hidden files. defaults to false 33 | - `index` Default file name, defaults to 'index.html' 34 | - `defer` If true, serves after `return next()`, allowing any downstream middleware to respond first. 35 | - `gzip` Try to serve the gzipped version of a file automatically when gzip is supported by a client and if the requested file with .gz extension exists. defaults to true. 36 | - `brotli` Try to serve the brotli version of a file automatically when brotli is supported by a client and if the requested file with .br extension exists (note, that brotli is only accepted over https). defaults to true. 37 | - [setHeaders](https://github.com/koajs/send#setheaders) Function to set custom headers on response. 38 | - `extensions` Try to match extensions from passed array to search for file when no extension is sufficed in URL. First found is served. (defaults to `false`). e.g. `['html']` 39 | 40 | ## Example 41 | 42 | ```js 43 | const serve = require("koa-static"); 44 | const Koa = require("koa"); 45 | const app = new Koa(); 46 | 47 | // $ GET /package.json 48 | app.use(serve(".")); 49 | 50 | // $ GET /hello.txt 51 | app.use(serve("test/fixtures")); 52 | 53 | // or use absolute paths 54 | app.use(serve(__dirname + "/test/fixtures")); 55 | 56 | app.listen(3000); 57 | 58 | console.log("listening on port 3000"); 59 | ``` 60 | 61 | ### See also 62 | 63 | - [koajs/conditional-get](https://github.com/koajs/conditional-get) Conditional GET support for koa 64 | - [koajs/compress](https://github.com/koajs/compress) Compress middleware for koa 65 | - [koajs/mount](https://github.com/koajs/mount) Mount `koa-static` to a specific path 66 | 67 | ## License 68 | 69 | MIT 70 | 71 | [npm-image]: https://img.shields.io/npm/v/koa-static.svg?style=flat-square 72 | [npm-url]: https://npmjs.org/package/koa-static 73 | [github-tag]: http://img.shields.io/github/tag/koajs/static.svg?style=flat-square 74 | [github-url]: https://github.com/koajs/static/tags 75 | [travis-image]: https://img.shields.io/travis/koajs/static.svg?style=flat-square 76 | [travis-url]: https://travis-ci.org/koajs/static 77 | [coveralls-image]: https://img.shields.io/coveralls/koajs/static.svg?style=flat-square 78 | [coveralls-url]: https://coveralls.io/r/koajs/static?branch=master 79 | [license-image]: http://img.shields.io/npm/l/koa-static.svg?style=flat-square 80 | [license-url]: LICENSE 81 | [downloads-image]: http://img.shields.io/npm/dm/koa-static.svg?style=flat-square 82 | [downloads-url]: https://npmjs.org/package/koa-static 83 | [gittip-image]: https://img.shields.io/gittip/jonathanong.svg?style=flat-square 84 | [gittip-url]: https://www.gittip.com/jonathanong/ 85 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | const serve = require('./') 5 | const Koa = require('koa') 6 | const app = new Koa() 7 | 8 | // $ GET /package.json 9 | // $ GET / 10 | 11 | app.use(serve('.')) 12 | 13 | app.use((ctx, next) => { 14 | return next().then(() => { 15 | if (ctx.path === '/') { 16 | ctx.body = 'Try `GET /package.json`' 17 | } 18 | }) 19 | }) 20 | 21 | app.listen(3000) 22 | 23 | console.log('listening on port 3000') 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | const debug = require('debug')('koa-static') 9 | const path = require('path') 10 | const assert = require('assert') 11 | const send = require('koa-send') 12 | 13 | /** 14 | * Expose `serve()`. 15 | */ 16 | 17 | module.exports = serve 18 | 19 | /** 20 | * Serve static files from `root`. 21 | * 22 | * @param {String} root 23 | * @param {Object} [opts] 24 | * @return {Function} 25 | * @api public 26 | */ 27 | 28 | function serve (root, opts = {}) { 29 | assert(root, 'root directory is required to serve files') 30 | 31 | debug('static "%s" %j', root, opts) 32 | opts.root = path.resolve(root) 33 | opts.index = opts.index ?? 'index.html' 34 | 35 | if (!opts.defer) { 36 | return async function serve (ctx, next) { 37 | let done = false 38 | 39 | if (ctx.method === 'HEAD' || ctx.method === 'GET') { 40 | try { 41 | done = await send(ctx, ctx.path, opts) 42 | } catch (err) { 43 | if (err.status !== 404) { 44 | throw err 45 | } 46 | } 47 | } 48 | 49 | if (!done) { 50 | await next() 51 | } 52 | } 53 | } 54 | 55 | return async function serve (ctx, next) { 56 | await next() 57 | 58 | if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return 59 | // response is already handled 60 | if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line 61 | 62 | try { 63 | await send(ctx, ctx.path, opts) 64 | } catch (err) { 65 | if (err.status !== 404) { 66 | throw err 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-static", 3 | "description": "Static file serving middleware for koa", 4 | "repository": "koajs/static", 5 | "version": "5.0.0", 6 | "keywords": [ 7 | "koa", 8 | "middleware", 9 | "file", 10 | "static", 11 | "sendfile" 12 | ], 13 | "files": [ 14 | "index.js" 15 | ], 16 | "devDependencies": { 17 | "eslint": "^8.16.0", 18 | "eslint-config-standard": "^17.0.0", 19 | "eslint-plugin-import": "^2.26.0", 20 | "eslint-plugin-n": "^15.2.0", 21 | "eslint-plugin-promise": "^6.0.0", 22 | "eslint-plugin-standard": "^5.0.0", 23 | "koa": "^2.13.4", 24 | "mocha": "^10.0.0", 25 | "nyc": "^15.1.0", 26 | "supertest": "^6.2.3" 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "debug": "^4.3.4", 31 | "koa-send": "^5.0.1" 32 | }, 33 | "scripts": { 34 | "lint": "eslint --fix .", 35 | "test": "mocha --exit", 36 | "test-cov": "nyc npm run test", 37 | "test-travis": "nyc npm run test -- --report lcovonly" 38 | }, 39 | "engines": { 40 | "node": ">= 14" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | mocha: true 4 | -------------------------------------------------------------------------------- /test/fixtures/hello.txt: -------------------------------------------------------------------------------- 1 | world -------------------------------------------------------------------------------- /test/fixtures/index.txt: -------------------------------------------------------------------------------- 1 | text index -------------------------------------------------------------------------------- /test/fixtures/world/index.html: -------------------------------------------------------------------------------- 1 | html index -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | const request = require('supertest') 5 | const assert = require('assert') 6 | const serve = require('..') 7 | const Koa = require('koa') 8 | 9 | describe('serve(root)', function () { 10 | describe('when defer: false', function () { 11 | describe('when root = "."', function () { 12 | it('should serve from cwd', function (done) { 13 | const app = new Koa() 14 | 15 | app.use(serve('.')) 16 | 17 | request(app.listen()) 18 | .get('/package.json') 19 | .expect(200, done) 20 | }) 21 | }) 22 | 23 | describe('when path is not a file', function () { 24 | it('should 404', function (done) { 25 | const app = new Koa() 26 | 27 | app.use(serve('test/fixtures')) 28 | 29 | request(app.listen()) 30 | .get('/something') 31 | .expect(404, done) 32 | }) 33 | 34 | it('should not throw 404 error', function (done) { 35 | const app = new Koa() 36 | 37 | let err = null 38 | 39 | app.use(async (ctx, next) => { 40 | try { 41 | await next() 42 | } catch (e) { 43 | err = e 44 | } 45 | }) 46 | 47 | app.use(serve('test/fixtures')) 48 | 49 | app.use(async (ctx) => { 50 | ctx.body = 'ok' 51 | }) 52 | 53 | request(app.listen()) 54 | .get('/something') 55 | .expect(200) 56 | .end((_, res) => { 57 | assert.equal(res.text, 'ok') 58 | assert.equal(err, null) 59 | done() 60 | }) 61 | }) 62 | }) 63 | 64 | describe('when upstream middleware responds', function () { 65 | it('should respond', function (done) { 66 | const app = new Koa() 67 | 68 | app.use(serve('test/fixtures')) 69 | 70 | app.use((ctx, next) => { 71 | return next().then(() => { 72 | ctx.body = 'hey' 73 | }) 74 | }) 75 | 76 | request(app.listen()) 77 | .get('/hello.txt') 78 | .expect(200) 79 | .expect('world', done) 80 | }) 81 | }) 82 | 83 | describe('the path is valid', function () { 84 | it('should serve the file', function (done) { 85 | const app = new Koa() 86 | 87 | app.use(serve('test/fixtures')) 88 | 89 | request(app.listen()) 90 | .get('/hello.txt') 91 | .expect(200) 92 | .expect('world', done) 93 | }) 94 | }) 95 | 96 | describe('.index', function () { 97 | describe('when present', function () { 98 | it('should alter the index file supported', function (done) { 99 | const app = new Koa() 100 | 101 | app.use(serve('test/fixtures', { index: 'index.txt' })) 102 | 103 | request(app.listen()) 104 | .get('/') 105 | .expect(200) 106 | .expect('Content-Type', 'text/plain; charset=utf-8') 107 | .expect('text index', done) 108 | }) 109 | }) 110 | 111 | describe('when omitted', function () { 112 | it('should use index.html', function (done) { 113 | const app = new Koa() 114 | 115 | app.use(serve('test/fixtures')) 116 | 117 | request(app.listen()) 118 | .get('/world/') 119 | .expect(200) 120 | .expect('Content-Type', 'text/html; charset=utf-8') 121 | .expect('html index', done) 122 | }) 123 | }) 124 | 125 | describe('when disabled', function () { 126 | it('should not use index.html', function (done) { 127 | const app = new Koa() 128 | 129 | app.use(serve('test/fixtures', { index: false })) 130 | 131 | request(app.listen()) 132 | .get('/world/') 133 | .expect(404, done) 134 | }) 135 | 136 | it('should pass to downstream if 404', function (done) { 137 | const app = new Koa() 138 | 139 | app.use(serve('test/fixtures', { index: false })) 140 | app.use(async (ctx) => { 141 | ctx.body = 'oh no' 142 | }) 143 | 144 | request(app.listen()) 145 | .get('/world/') 146 | .expect('oh no', done) 147 | }) 148 | }) 149 | }) 150 | 151 | describe('when method is not `GET` or `HEAD`', function () { 152 | it('should 404', function (done) { 153 | const app = new Koa() 154 | 155 | app.use(serve('test/fixtures')) 156 | 157 | request(app.listen()) 158 | .post('/hello.txt') 159 | .expect(404, done) 160 | }) 161 | }) 162 | }) 163 | 164 | describe('when defer: true', function () { 165 | describe('when upstream middleware responds', function () { 166 | it('should do nothing', function (done) { 167 | const app = new Koa() 168 | 169 | app.use(serve('test/fixtures', { 170 | defer: true 171 | })) 172 | 173 | app.use((ctx, next) => { 174 | return next().then(() => { 175 | ctx.body = 'hey' 176 | }) 177 | }) 178 | 179 | request(app.listen()) 180 | .get('/hello.txt') 181 | .expect(200) 182 | .expect('hey', done) 183 | }) 184 | }) 185 | 186 | describe('the path is valid', function () { 187 | it('should serve the file', function (done) { 188 | const app = new Koa() 189 | 190 | app.use(serve('test/fixtures', { 191 | defer: true 192 | })) 193 | 194 | request(app.listen()) 195 | .get('/hello.txt') 196 | .expect(200) 197 | .expect('world', done) 198 | }) 199 | }) 200 | 201 | describe('.index', function () { 202 | describe('when present', function () { 203 | it('should alter the index file supported', function (done) { 204 | const app = new Koa() 205 | 206 | app.use(serve('test/fixtures', { 207 | defer: true, 208 | index: 'index.txt' 209 | })) 210 | 211 | request(app.listen()) 212 | .get('/') 213 | .expect(200) 214 | .expect('Content-Type', 'text/plain; charset=utf-8') 215 | .expect('text index', done) 216 | }) 217 | }) 218 | 219 | describe('when omitted', function () { 220 | it('should use index.html', function (done) { 221 | const app = new Koa() 222 | 223 | app.use(serve('test/fixtures', { 224 | defer: true 225 | })) 226 | 227 | request(app.listen()) 228 | .get('/world/') 229 | .expect(200) 230 | .expect('Content-Type', 'text/html; charset=utf-8') 231 | .expect('html index', done) 232 | }) 233 | }) 234 | }) 235 | 236 | // describe('when path is a directory', function(){ 237 | // describe('and an index file is present', function(){ 238 | // it('should redirect missing / to -> / when index is found', function(done){ 239 | // const app = new Koa(); 240 | 241 | // app.use(serve('test/fixtures')); 242 | 243 | // request(app.listen()) 244 | // .get('/world') 245 | // .expect(303) 246 | // .expect('Location', '/world/', done); 247 | // }) 248 | // }) 249 | 250 | // describe('and no index file is present', function(){ 251 | // it('should not redirect', function(done){ 252 | // const app = new Koa(); 253 | 254 | // app.use(serve('test/fixtures')); 255 | 256 | // request(app.listen()) 257 | // .get('/') 258 | // .expect(404, done); 259 | // }) 260 | // }) 261 | // }) 262 | 263 | describe('when path is not a file', function () { 264 | it('should 404', function (done) { 265 | const app = new Koa() 266 | 267 | app.use(serve('test/fixtures', { 268 | defer: true 269 | })) 270 | 271 | request(app.listen()) 272 | .get('/something') 273 | .expect(404, done) 274 | }) 275 | 276 | it('should not throw 404 error', function (done) { 277 | const app = new Koa() 278 | 279 | let err = null 280 | 281 | app.use(async (ctx, next) => { 282 | try { 283 | await next() 284 | } catch (e) { 285 | err = e 286 | } 287 | }) 288 | 289 | app.use(serve('test/fixtures', { 290 | defer: true 291 | })) 292 | 293 | request(app.listen()) 294 | .get('/something') 295 | .expect(200) 296 | .end((_, res) => { 297 | assert.equal(err, null) 298 | done() 299 | }) 300 | }) 301 | }) 302 | 303 | describe('it should not handle the request', function () { 304 | it('when status=204', function (done) { 305 | const app = new Koa() 306 | 307 | app.use(serve('test/fixtures', { 308 | defer: true 309 | })) 310 | 311 | app.use((ctx) => { 312 | ctx.status = 204 313 | }) 314 | 315 | request(app.listen()) 316 | .get('/something%%%/') 317 | .expect(204, done) 318 | }) 319 | 320 | it('when body=""', function (done) { 321 | const app = new Koa() 322 | 323 | app.use(serve('test/fixtures', { 324 | defer: true 325 | })) 326 | 327 | app.use((ctx) => { 328 | ctx.body = '' 329 | }) 330 | 331 | request(app.listen()) 332 | .get('/something%%%/') 333 | .expect(200, done) 334 | }) 335 | }) 336 | 337 | describe('when method is not `GET` or `HEAD`', function () { 338 | it('should 404', function (done) { 339 | const app = new Koa() 340 | 341 | app.use(serve('test/fixtures', { 342 | defer: true 343 | })) 344 | 345 | request(app.listen()) 346 | .post('/hello.txt') 347 | .expect(404, done) 348 | }) 349 | }) 350 | }) 351 | 352 | describe('option - format', function () { 353 | describe('when format: false', function () { 354 | it('should 404', function (done) { 355 | const app = new Koa() 356 | 357 | app.use(serve('test/fixtures', { 358 | index: 'index.html', 359 | format: false 360 | })) 361 | 362 | request(app.listen()) 363 | .get('/world') 364 | .expect(404, done) 365 | }) 366 | 367 | it('should 200', function (done) { 368 | const app = new Koa() 369 | 370 | app.use(serve('test/fixtures', { 371 | index: 'index.html', 372 | format: false 373 | })) 374 | 375 | request(app.listen()) 376 | .get('/world/') 377 | .expect(200, done) 378 | }) 379 | }) 380 | 381 | describe('when format: true', function () { 382 | it('should 200', function (done) { 383 | const app = new Koa() 384 | 385 | app.use(serve('test/fixtures', { 386 | index: 'index.html', 387 | format: true 388 | })) 389 | 390 | request(app.listen()) 391 | .get('/world') 392 | .expect(200, done) 393 | }) 394 | 395 | it('should 200', function (done) { 396 | const app = new Koa() 397 | 398 | app.use(serve('test/fixtures', { 399 | index: 'index.html', 400 | format: true 401 | })) 402 | 403 | request(app.listen()) 404 | .get('/world/') 405 | .expect(200, done) 406 | }) 407 | }) 408 | }) 409 | }) 410 | --------------------------------------------------------------------------------