Comparing version 4.0.0 to 4.1.0
4.1.0 / 2017-04-26 | ||
================== | ||
* Add support for Cache-Control: immutable with a new "immutable" option | ||
* Added serving of brotli versions of files | ||
4.0.0 / 2017-04-09 | ||
@@ -3,0 +9,0 @@ ================== |
138
index.js
@@ -5,7 +5,7 @@ /** | ||
const debug = require('debug')('koa-send'); | ||
const resolvePath = require('resolve-path'); | ||
const createError = require('http-errors'); | ||
const assert = require('assert'); | ||
const fs = require('mz/fs'); | ||
const debug = require('debug')('koa-send') | ||
const resolvePath = require('resolve-path') | ||
const createError = require('http-errors') | ||
const assert = require('assert') | ||
const fs = require('mz/fs') | ||
@@ -19,3 +19,3 @@ const { | ||
sep | ||
} = require('path'); | ||
} = require('path') | ||
@@ -26,3 +26,3 @@ /** | ||
module.exports = send; | ||
module.exports = send | ||
@@ -40,18 +40,20 @@ /** | ||
async function send(ctx, path, opts = {}) { | ||
assert(ctx, 'koa context required'); | ||
assert(path, 'pathname required'); | ||
async function send (ctx, path, opts = {}) { | ||
assert(ctx, 'koa context required') | ||
assert(path, 'pathname required') | ||
// options | ||
debug('send "%s" %j', path, opts); | ||
const root = opts.root ? normalize(resolve(opts.root)) : ''; | ||
const trailingSlash = '/' == path[path.length - 1]; | ||
path = path.substr(parse(path).root.length); | ||
const index = opts.index; | ||
const maxage = opts.maxage || opts.maxAge || 0; | ||
const hidden = opts.hidden || false; | ||
const format = opts.format === false ? false : true; | ||
const extensions = Array.isArray(opts.extensions) ? opts.extensions : false; | ||
const gzip = opts.gzip === false ? false : true; | ||
const setHeaders = opts.setHeaders; | ||
debug('send "%s" %j', path, opts) | ||
const root = opts.root ? normalize(resolve(opts.root)) : '' | ||
const trailingSlash = path[path.length - 1] === '/' | ||
path = path.substr(parse(path).root.length) | ||
const index = opts.index | ||
const maxage = opts.maxage || opts.maxAge || 0 | ||
const immutable = opts.immutable || false | ||
const hidden = opts.hidden || false | ||
const format = opts.format !== false | ||
const extensions = Array.isArray(opts.extensions) ? opts.extensions : false | ||
const brotli = opts.brotli !== false | ||
const gzip = opts.gzip !== false | ||
const setHeaders = opts.setHeaders | ||
@@ -62,35 +64,37 @@ if (setHeaders && typeof setHeaders !== 'function') { | ||
const encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity'); | ||
// normalize path | ||
path = decode(path); | ||
path = decode(path) | ||
if (-1 == path) return ctx.throw(400, 'failed to decode'); | ||
if (path === -1) return ctx.throw(400, 'failed to decode') | ||
// index file support | ||
if (index && trailingSlash) path += index; | ||
if (index && trailingSlash) path += index | ||
path = resolvePath(root, path); | ||
path = resolvePath(root, path) | ||
// hidden file support, ignore | ||
if (!hidden && isHidden(root, path)) return; | ||
if (!hidden && isHidden(root, path)) return | ||
// serve gzipped file when possible | ||
if (encoding === 'gzip' && gzip && (await fs.exists(path + '.gz'))) { | ||
path = path + '.gz'; | ||
ctx.set('Content-Encoding', 'gzip'); | ||
ctx.res.removeHeader('Content-Length'); | ||
// serve brotli file when possible otherwise gzipped file when possible | ||
if (ctx.acceptsEncodings('br', 'deflate', 'identity') === 'br' && brotli && (await fs.exists(path + '.br'))) { | ||
path = path + '.br' | ||
ctx.set('Content-Encoding', 'br') | ||
ctx.res.removeHeader('Content-Length') | ||
} else if (ctx.acceptsEncodings('gzip', 'deflate', 'identity') === 'gzip' && gzip && (await fs.exists(path + '.gz'))) { | ||
path = path + '.gz' | ||
ctx.set('Content-Encoding', 'gzip') | ||
ctx.res.removeHeader('Content-Length') | ||
} | ||
if (extensions && !/\..*$/.exec(path)) { | ||
const list = [].concat(extensions); | ||
const list = [].concat(extensions) | ||
for (let i = 0; i < list.length; i++) { | ||
let ext = list[i]; | ||
let ext = list[i] | ||
if (typeof ext !== 'string') { | ||
throw new TypeError('option extensions must be array of strings or false'); | ||
throw new TypeError('option extensions must be array of strings or false') | ||
} | ||
if (!/^\./.exec(ext)) ext = '.' + ext; | ||
if (!/^\./.exec(ext)) ext = '.' + ext | ||
if (await fs.exists(path + ext)) { | ||
path = path + ext; | ||
break; | ||
path = path + ext | ||
break | ||
} | ||
@@ -103,3 +107,3 @@ } | ||
try { | ||
stats = await fs.stat(path); | ||
stats = await fs.stat(path) | ||
@@ -111,27 +115,33 @@ // Format the path to serve static file servers | ||
if (format && index) { | ||
path += '/' + index; | ||
stats = await fs.stat(path); | ||
path += '/' + index | ||
stats = await fs.stat(path) | ||
} else { | ||
return; | ||
return | ||
} | ||
} | ||
} catch (err) { | ||
const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; | ||
const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'] | ||
if (notfound.includes(err.code)) { | ||
throw createError(404, err); | ||
throw createError(404, err) | ||
} | ||
err.status = 500; | ||
throw err; | ||
err.status = 500 | ||
throw err | ||
} | ||
if (setHeaders) setHeaders(ctx.res, path, stats); | ||
if (setHeaders) setHeaders(ctx.res, path, stats) | ||
// stream | ||
ctx.set('Content-Length', stats.size); | ||
if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()); | ||
if (!ctx.response.get('Cache-Control')) ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0)); | ||
ctx.type = type(path); | ||
ctx.body = fs.createReadStream(path); | ||
ctx.set('Content-Length', stats.size) | ||
if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()) | ||
if (!ctx.response.get('Cache-Control')) { | ||
const directives = ['max-age=' + (maxage / 1000 | 0)] | ||
if (immutable) { | ||
directives.push('immutable') | ||
} | ||
ctx.set('Cache-Control', directives.join(',')) | ||
} | ||
ctx.type = type(path) | ||
ctx.body = fs.createReadStream(path) | ||
return path; | ||
return path | ||
} | ||
@@ -143,8 +153,8 @@ | ||
function isHidden(root, path) { | ||
path = path.substr(root.length).split(sep); | ||
for(let i = 0; i < path.length; i++) { | ||
if(path[i][0] === '.') return true; | ||
function isHidden (root, path) { | ||
path = path.substr(root.length).split(sep) | ||
for (let i = 0; i < path.length; i++) { | ||
if (path[i][0] === '.') return true | ||
} | ||
return false; | ||
return false | ||
} | ||
@@ -156,4 +166,4 @@ | ||
function type(file) { | ||
return extname(basename(file, '.gz')); | ||
function type (file) { | ||
return extname(basename(file, '.gz')) | ||
} | ||
@@ -165,8 +175,8 @@ | ||
function decode(path) { | ||
function decode (path) { | ||
try { | ||
return decodeURIComponent(path); | ||
return decodeURIComponent(path) | ||
} catch (err) { | ||
return -1; | ||
return -1 | ||
} | ||
} |
@@ -5,3 +5,3 @@ { | ||
"repository": "koajs/send", | ||
"version": "4.0.0", | ||
"version": "4.1.0", | ||
"keywords": [ | ||
@@ -17,2 +17,9 @@ "koa", | ||
"devDependencies": { | ||
"eslint": "^3.19.0", | ||
"eslint-config-standard": "^10.2.1", | ||
"eslint-plugin-import": "^2.2.0", | ||
"eslint-plugin-node": "^4.2.2", | ||
"eslint-plugin-promise": "^3.5.0", | ||
"eslint-plugin-standard": "^3.0.1", | ||
"iltorb": "^1.2.1", | ||
"istanbul": "0", | ||
@@ -32,2 +39,3 @@ "koa": "2", | ||
"scripts": { | ||
"lint": "eslint --fix .", | ||
"test": "mocha --require should --reporter spec", | ||
@@ -34,0 +42,0 @@ "test-cov": "istanbul cover ./node_modules/.bin/_mocha -- --require should", |
@@ -22,5 +22,7 @@ | ||
- `maxage` Browser cache max-age in milliseconds. (defaults to `0`) | ||
- `immutable` Tell the browser the resource is immutable and can be cached indefinitely (defaults to `false`) | ||
- `hidden` Allow transfer of hidden files. (defaults to `false`) | ||
- [`root`](#root-path) Root directory to restrict file access | ||
- `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. | ||
- `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. defaults to true. | ||
- `format` If not `false` (defaults to `true`), format the path to serve static file servers and not require a trailing slash for directories, so that you can do both `/directory` and `/directory/` | ||
@@ -27,0 +29,0 @@ - [`setHeaders`](#setheaders) Function to set custom headers on response. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11519
146
105
12