send
Advanced tools
Comparing version 0.8.5 to 0.9.0
@@ -0,1 +1,8 @@ | ||
0.9.0 / 2014-09-07 | ||
================== | ||
* Add `lastMofified` option | ||
* Use `etag` to generate `ETag` header | ||
* deps: debug@~2.0.0 | ||
0.8.5 / 2014-09-04 | ||
@@ -2,0 +9,0 @@ ================== |
773
index.js
module.exports = require('./lib/send'); | ||
/** | ||
* Module dependencies. | ||
*/ | ||
var debug = require('debug')('send') | ||
var deprecate = require('depd')('send') | ||
var destroy = require('destroy') | ||
var escapeHtml = require('escape-html') | ||
, parseRange = require('range-parser') | ||
, Stream = require('stream') | ||
, mime = require('mime') | ||
, fresh = require('fresh') | ||
, path = require('path') | ||
, http = require('http') | ||
, fs = require('fs') | ||
, normalize = path.normalize | ||
, join = path.join | ||
var etag = require('etag') | ||
var EventEmitter = require('events').EventEmitter; | ||
var ms = require('ms'); | ||
var onFinished = require('on-finished') | ||
/** | ||
* Variables. | ||
*/ | ||
var extname = path.extname | ||
var maxMaxAge = 60 * 60 * 24 * 365 * 1000; // 1 year | ||
var resolve = path.resolve | ||
var sep = path.sep | ||
var toString = Object.prototype.toString | ||
var upPathRegexp = /(?:^|[\\\/])\.\.(?:[\\\/]|$)/ | ||
/** | ||
* Expose `send`. | ||
*/ | ||
exports = module.exports = send; | ||
/** | ||
* Expose mime module. | ||
*/ | ||
exports.mime = mime; | ||
/** | ||
* Shim EventEmitter.listenerCount for node.js < 0.10 | ||
*/ | ||
/* istanbul ignore next */ | ||
var listenerCount = EventEmitter.listenerCount | ||
|| function(emitter, type){ return emitter.listeners(type).length; }; | ||
/** | ||
* Return a `SendStream` for `req` and `path`. | ||
* | ||
* @param {Request} req | ||
* @param {String} path | ||
* @param {Object} options | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
function send(req, path, options) { | ||
return new SendStream(req, path, options); | ||
} | ||
/** | ||
* Initialize a `SendStream` with the given `path`. | ||
* | ||
* @param {Request} req | ||
* @param {String} path | ||
* @param {Object} options | ||
* @api private | ||
*/ | ||
function SendStream(req, path, options) { | ||
var self = this; | ||
options = options || {}; | ||
this.req = req; | ||
this.path = path; | ||
this.options = options; | ||
this._etag = options.etag !== undefined | ||
? Boolean(options.etag) | ||
: true | ||
this._dotfiles = options.dotfiles !== undefined | ||
? options.dotfiles | ||
: 'ignore' | ||
if (['allow', 'deny', 'ignore'].indexOf(this._dotfiles) === -1) { | ||
throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') | ||
} | ||
this._hidden = Boolean(options.hidden) | ||
if ('hidden' in options) { | ||
deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead') | ||
} | ||
// legacy support | ||
if (!('dotfiles' in options)) { | ||
this._dotfiles = undefined | ||
} | ||
this._extensions = options.extensions !== undefined | ||
? normalizeList(options.extensions) | ||
: [] | ||
this._index = options.index !== undefined | ||
? normalizeList(options.index) | ||
: ['index.html'] | ||
this._lastModified = options.lastModified !== undefined | ||
? Boolean(options.lastModified) | ||
: true | ||
this._maxage = options.maxAge || options.maxage | ||
this._maxage = typeof this._maxage === 'string' | ||
? ms(this._maxage) | ||
: Number(this._maxage) | ||
this._maxage = !isNaN(this._maxage) | ||
? Math.min(Math.max(0, this._maxage), maxMaxAge) | ||
: 0 | ||
this._root = options.root | ||
? resolve(options.root) | ||
: null | ||
if (!this._root && options.from) { | ||
this.from(options.from); | ||
} | ||
} | ||
/** | ||
* Inherits from `Stream.prototype`. | ||
*/ | ||
SendStream.prototype.__proto__ = Stream.prototype; | ||
/** | ||
* Enable or disable etag generation. | ||
* | ||
* @param {Boolean} val | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.etag = deprecate.function(function etag(val) { | ||
val = Boolean(val); | ||
debug('etag %s', val); | ||
this._etag = val; | ||
return this; | ||
}, 'send.etag: pass etag as option'); | ||
/** | ||
* Enable or disable "hidden" (dot) files. | ||
* | ||
* @param {Boolean} path | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.hidden = deprecate.function(function hidden(val) { | ||
val = Boolean(val); | ||
debug('hidden %s', val); | ||
this._hidden = val; | ||
this._dotfiles = undefined | ||
return this; | ||
}, 'send.hidden: use dotfiles option'); | ||
/** | ||
* Set index `paths`, set to a falsy | ||
* value to disable index support. | ||
* | ||
* @param {String|Boolean|Array} paths | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.index = deprecate.function(function index(paths) { | ||
var index = !paths ? [] : normalizeList(paths); | ||
debug('index %o', paths); | ||
this._index = index; | ||
return this; | ||
}, 'send.index: pass index as option'); | ||
/** | ||
* Set root `path`. | ||
* | ||
* @param {String} path | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.root = function(path){ | ||
path = String(path); | ||
this._root = resolve(path) | ||
return this; | ||
}; | ||
SendStream.prototype.from = deprecate.function(SendStream.prototype.root, | ||
'send.from: pass root as option'); | ||
SendStream.prototype.root = deprecate.function(SendStream.prototype.root, | ||
'send.root: pass root as option'); | ||
/** | ||
* Set max-age to `maxAge`. | ||
* | ||
* @param {Number} maxAge | ||
* @return {SendStream} | ||
* @api public | ||
*/ | ||
SendStream.prototype.maxage = deprecate.function(function maxage(maxAge) { | ||
maxAge = typeof maxAge === 'string' | ||
? ms(maxAge) | ||
: Number(maxAge); | ||
if (isNaN(maxAge)) maxAge = 0; | ||
if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000; | ||
debug('max-age %d', maxAge); | ||
this._maxage = maxAge; | ||
return this; | ||
}, 'send.maxage: pass maxAge as option'); | ||
/** | ||
* Emit error with `status`. | ||
* | ||
* @param {Number} status | ||
* @api private | ||
*/ | ||
SendStream.prototype.error = function(status, err){ | ||
var res = this.res; | ||
var msg = http.STATUS_CODES[status]; | ||
err = err || new Error(msg); | ||
err.status = status; | ||
// emit if listeners instead of responding | ||
if (listenerCount(this, 'error') !== 0) { | ||
return this.emit('error', err); | ||
} | ||
// wipe all existing headers | ||
res._headers = undefined; | ||
res.statusCode = err.status; | ||
res.end(msg); | ||
}; | ||
/** | ||
* Check if the pathname ends with "/". | ||
* | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
SendStream.prototype.hasTrailingSlash = function(){ | ||
return '/' == this.path[this.path.length - 1]; | ||
}; | ||
/** | ||
* Check if this is a conditional GET request. | ||
* | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
SendStream.prototype.isConditionalGET = function(){ | ||
return this.req.headers['if-none-match'] | ||
|| this.req.headers['if-modified-since']; | ||
}; | ||
/** | ||
* Strip content-* header fields. | ||
* | ||
* @api private | ||
*/ | ||
SendStream.prototype.removeContentHeaderFields = function(){ | ||
var res = this.res; | ||
Object.keys(res._headers).forEach(function(field){ | ||
if (0 == field.indexOf('content')) { | ||
res.removeHeader(field); | ||
} | ||
}); | ||
}; | ||
/** | ||
* Respond with 304 not modified. | ||
* | ||
* @api private | ||
*/ | ||
SendStream.prototype.notModified = function(){ | ||
var res = this.res; | ||
debug('not modified'); | ||
this.removeContentHeaderFields(); | ||
res.statusCode = 304; | ||
res.end(); | ||
}; | ||
/** | ||
* Raise error that headers already sent. | ||
* | ||
* @api private | ||
*/ | ||
SendStream.prototype.headersAlreadySent = function headersAlreadySent(){ | ||
var err = new Error('Can\'t set headers after they are sent.'); | ||
debug('headers already sent'); | ||
this.error(500, err); | ||
}; | ||
/** | ||
* Check if the request is cacheable, aka | ||
* responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}). | ||
* | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
SendStream.prototype.isCachable = function(){ | ||
var res = this.res; | ||
return (res.statusCode >= 200 && res.statusCode < 300) || 304 == res.statusCode; | ||
}; | ||
/** | ||
* Handle stat() error. | ||
* | ||
* @param {Error} err | ||
* @api private | ||
*/ | ||
SendStream.prototype.onStatError = function(err){ | ||
var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; | ||
if (~notfound.indexOf(err.code)) return this.error(404, err); | ||
this.error(500, err); | ||
}; | ||
/** | ||
* Check if the cache is fresh. | ||
* | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
SendStream.prototype.isFresh = function(){ | ||
return fresh(this.req.headers, this.res._headers); | ||
}; | ||
/** | ||
* Check if the range is fresh. | ||
* | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
SendStream.prototype.isRangeFresh = function isRangeFresh(){ | ||
var ifRange = this.req.headers['if-range']; | ||
if (!ifRange) return true; | ||
return ~ifRange.indexOf('"') | ||
? ~ifRange.indexOf(this.res._headers['etag']) | ||
: Date.parse(this.res._headers['last-modified']) <= Date.parse(ifRange); | ||
}; | ||
/** | ||
* Redirect to `path`. | ||
* | ||
* @param {String} path | ||
* @api private | ||
*/ | ||
SendStream.prototype.redirect = function(path){ | ||
if (listenerCount(this, 'directory') !== 0) { | ||
return this.emit('directory'); | ||
} | ||
if (this.hasTrailingSlash()) return this.error(403); | ||
var res = this.res; | ||
path += '/'; | ||
res.statusCode = 301; | ||
res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||
res.setHeader('Location', path); | ||
res.end('Redirecting to <a href="' + escapeHtml(path) + '">' + escapeHtml(path) + '</a>\n'); | ||
}; | ||
/** | ||
* Pipe to `res. | ||
* | ||
* @param {Stream} res | ||
* @return {Stream} res | ||
* @api public | ||
*/ | ||
SendStream.prototype.pipe = function(res){ | ||
var self = this | ||
, args = arguments | ||
, root = this._root; | ||
// references | ||
this.res = res; | ||
// decode the path | ||
var path = decode(this.path) | ||
if (path === -1) return this.error(400) | ||
// null byte(s) | ||
if (~path.indexOf('\0')) return this.error(400); | ||
var parts | ||
if (root !== null) { | ||
// join / normalize from optional root dir | ||
path = normalize(join(root, path)) | ||
root = normalize(root + sep) | ||
// malicious path | ||
if ((path + sep).substr(0, root.length) !== root) { | ||
debug('malicious path "%s"', path) | ||
return this.error(403) | ||
} | ||
// explode path parts | ||
parts = path.substr(root.length).split(sep) | ||
} else { | ||
// ".." is malicious without "root" | ||
if (upPathRegexp.test(path)) { | ||
debug('malicious path "%s"', path) | ||
return this.error(403) | ||
} | ||
// explode path parts | ||
parts = normalize(path).split(sep) | ||
// resolve the path | ||
path = resolve(path) | ||
} | ||
// dotfile handling | ||
if (containsDotFile(parts)) { | ||
var access = this._dotfiles | ||
// legacy support | ||
if (access === undefined) { | ||
access = parts[parts.length - 1][0] === '.' | ||
? (this._hidden ? 'allow' : 'ignore') | ||
: 'allow' | ||
} | ||
debug('%s dotfile "%s"', access, path) | ||
switch (access) { | ||
case 'allow': | ||
break | ||
case 'deny': | ||
return this.error(403) | ||
case 'ignore': | ||
default: | ||
return this.error(404) | ||
} | ||
} | ||
// index file support | ||
if (this._index.length && this.path[this.path.length - 1] === '/') { | ||
this.sendIndex(path); | ||
return res; | ||
} | ||
this.sendFile(path); | ||
return res; | ||
}; | ||
/** | ||
* Transfer `path`. | ||
* | ||
* @param {String} path | ||
* @api public | ||
*/ | ||
SendStream.prototype.send = function(path, stat){ | ||
var options = this.options; | ||
var len = stat.size; | ||
var res = this.res; | ||
var req = this.req; | ||
var ranges = req.headers.range; | ||
var offset = options.start || 0; | ||
if (res._header) { | ||
// impossible to send now | ||
return this.headersAlreadySent(); | ||
} | ||
debug('pipe "%s"', path) | ||
// set header fields | ||
this.setHeader(path, stat); | ||
// set content-type | ||
this.type(path); | ||
// conditional GET support | ||
if (this.isConditionalGET() | ||
&& this.isCachable() | ||
&& this.isFresh()) { | ||
return this.notModified(); | ||
} | ||
// adjust len to start/end options | ||
len = Math.max(0, len - offset); | ||
if (options.end !== undefined) { | ||
var bytes = options.end - offset + 1; | ||
if (len > bytes) len = bytes; | ||
} | ||
// Range support | ||
if (ranges) { | ||
ranges = parseRange(len, ranges); | ||
// If-Range support | ||
if (!this.isRangeFresh()) { | ||
debug('range stale'); | ||
ranges = -2; | ||
} | ||
// unsatisfiable | ||
if (-1 == ranges) { | ||
debug('range unsatisfiable'); | ||
res.setHeader('Content-Range', 'bytes */' + stat.size); | ||
return this.error(416); | ||
} | ||
// valid (syntactically invalid/multiple ranges are treated as a regular response) | ||
if (-2 != ranges && ranges.length === 1) { | ||
debug('range %j', ranges); | ||
options.start = offset + ranges[0].start; | ||
options.end = offset + ranges[0].end; | ||
// Content-Range | ||
res.statusCode = 206; | ||
res.setHeader('Content-Range', 'bytes ' | ||
+ ranges[0].start | ||
+ '-' | ||
+ ranges[0].end | ||
+ '/' | ||
+ len); | ||
len = options.end - options.start + 1; | ||
} | ||
} | ||
// content-length | ||
res.setHeader('Content-Length', len); | ||
// HEAD support | ||
if ('HEAD' == req.method) return res.end(); | ||
this.stream(path, options); | ||
}; | ||
/** | ||
* Transfer file for `path`. | ||
* | ||
* @param {String} path | ||
* @api private | ||
*/ | ||
SendStream.prototype.sendFile = function sendFile(path) { | ||
var i = 0 | ||
var self = this | ||
debug('stat "%s"', path); | ||
fs.stat(path, function onstat(err, stat) { | ||
if (err && err.code === 'ENOENT' | ||
&& !extname(path) | ||
&& path[path.length - 1] !== sep) { | ||
// not found, check extensions | ||
return next(err) | ||
} | ||
if (err) return self.onStatError(err) | ||
if (stat.isDirectory()) return self.redirect(self.path) | ||
self.emit('file', path, stat) | ||
self.send(path, stat) | ||
}) | ||
function next(err) { | ||
if (self._extensions.length <= i) { | ||
return err | ||
? self.onStatError(err) | ||
: self.error(404) | ||
} | ||
var p = path + '.' + self._extensions[i++] | ||
debug('stat "%s"', p) | ||
fs.stat(p, function (err, stat) { | ||
if (err) return next(err) | ||
if (stat.isDirectory()) return next() | ||
self.emit('file', p, stat) | ||
self.send(p, stat) | ||
}) | ||
} | ||
} | ||
/** | ||
* Transfer index for `path`. | ||
* | ||
* @param {String} path | ||
* @api private | ||
*/ | ||
SendStream.prototype.sendIndex = function sendIndex(path){ | ||
var i = -1; | ||
var self = this; | ||
function next(err){ | ||
if (++i >= self._index.length) { | ||
if (err) return self.onStatError(err); | ||
return self.error(404); | ||
} | ||
var p = join(path, self._index[i]); | ||
debug('stat "%s"', p); | ||
fs.stat(p, function(err, stat){ | ||
if (err) return next(err); | ||
if (stat.isDirectory()) return next(); | ||
self.emit('file', p, stat); | ||
self.send(p, stat); | ||
}); | ||
} | ||
next(); | ||
}; | ||
/** | ||
* Stream `path` to the response. | ||
* | ||
* @param {String} path | ||
* @param {Object} options | ||
* @api private | ||
*/ | ||
SendStream.prototype.stream = function(path, options){ | ||
// TODO: this is all lame, refactor meeee | ||
var finished = false; | ||
var self = this; | ||
var res = this.res; | ||
var req = this.req; | ||
// pipe | ||
var stream = fs.createReadStream(path, options); | ||
this.emit('stream', stream); | ||
stream.pipe(res); | ||
// response finished, done with the fd | ||
onFinished(res, function onfinished(){ | ||
finished = true; | ||
destroy(stream); | ||
}); | ||
// error handling code-smell | ||
stream.on('error', function onerror(err){ | ||
// request already finished | ||
if (finished) return; | ||
// clean up stream | ||
finished = true; | ||
destroy(stream); | ||
// error | ||
self.onStatError(err); | ||
}); | ||
// end | ||
stream.on('end', function onend(){ | ||
self.emit('end'); | ||
}); | ||
}; | ||
/** | ||
* Set content-type based on `path` | ||
* if it hasn't been explicitly set. | ||
* | ||
* @param {String} path | ||
* @api private | ||
*/ | ||
SendStream.prototype.type = function(path){ | ||
var res = this.res; | ||
if (res.getHeader('Content-Type')) return; | ||
var type = mime.lookup(path); | ||
var charset = mime.charsets.lookup(type); | ||
debug('content-type %s', type); | ||
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')); | ||
}; | ||
/** | ||
* Set response header fields, most | ||
* fields may be pre-defined. | ||
* | ||
* @param {String} path | ||
* @param {Object} stat | ||
* @api private | ||
*/ | ||
SendStream.prototype.setHeader = function setHeader(path, stat){ | ||
var res = this.res; | ||
this.emit('headers', res, path, stat); | ||
if (!res.getHeader('Accept-Ranges')) res.setHeader('Accept-Ranges', 'bytes'); | ||
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString()); | ||
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + Math.floor(this._maxage / 1000)); | ||
if (this._lastModified && !res.getHeader('Last-Modified')) { | ||
var modified = stat.mtime.toUTCString() | ||
debug('modified %s', modified) | ||
res.setHeader('Last-Modified', modified) | ||
} | ||
if (this._etag && !res.getHeader('ETag')) { | ||
var val = etag(stat) | ||
debug('etag %s', val) | ||
res.setHeader('ETag', val) | ||
} | ||
}; | ||
/** | ||
* Determine if path parts contain a dotfile. | ||
* | ||
* @api private | ||
*/ | ||
function containsDotFile(parts) { | ||
for (var i = 0; i < parts.length; i++) { | ||
if (parts[i][0] === '.') { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
/** | ||
* decodeURIComponent. | ||
* | ||
* Allows V8 to only deoptimize this fn instead of all | ||
* of send(). | ||
* | ||
* @param {String} path | ||
* @api private | ||
*/ | ||
function decode(path) { | ||
try { | ||
return decodeURIComponent(path) | ||
} catch (err) { | ||
return -1 | ||
} | ||
} | ||
/** | ||
* Normalize the index option into an array. | ||
* | ||
* @param {boolean|string|array} val | ||
* @api private | ||
*/ | ||
function normalizeList(val){ | ||
return [].concat(val || []) | ||
} |
{ | ||
"name": "send", | ||
"description": "Better streaming static file server with Range and conditional-GET support", | ||
"version": "0.8.5", | ||
"version": "0.9.0", | ||
"author": "TJ Holowaychuk <tj@vision-media.ca>", | ||
@@ -17,6 +17,7 @@ "contributors": [ | ||
"dependencies": { | ||
"debug": "1.0.4", | ||
"debug": "~2.0.0", | ||
"depd": "0.4.4", | ||
"destroy": "1.0.3", | ||
"escape-html": "1.0.1", | ||
"etag": "~1.3.0", | ||
"fresh": "0.2.2", | ||
@@ -34,2 +35,7 @@ "mime": "1.2.11", | ||
}, | ||
"files": [ | ||
"History.md", | ||
"LICENSE", | ||
"index.js" | ||
], | ||
"engines": { | ||
@@ -36,0 +42,0 @@ "node": ">= 0.8.0" |
173
Readme.md
# send | ||
[![NPM version](https://badge.fury.io/js/send.svg)](https://badge.fury.io/js/send) | ||
[![Build Status](https://travis-ci.org/visionmedia/send.svg?branch=master)](https://travis-ci.org/visionmedia/send) | ||
[![Coverage Status](https://img.shields.io/coveralls/visionmedia/send.svg?branch=master)](https://coveralls.io/r/visionmedia/send) | ||
[![Gittip](http://img.shields.io/gittip/dougwilson.svg)](https://www.gittip.com/dougwilson/) | ||
[![NPM Version][npm-image]][npm-url] | ||
[![NPM Downloads][downloads-image]][downloads-url] | ||
[![Build Status][travis-image]][travis-url] | ||
[![Test Coverage][coveralls-image]][coveralls-url] | ||
[![Gittip][gittip-image]][gittip-url] | ||
@@ -13,64 +14,22 @@ Send is Connect's `static()` extracted for generalized use, a streaming static file | ||
$ npm install send | ||
## Examples | ||
Small: | ||
```js | ||
var http = require('http'); | ||
var send = require('send'); | ||
var app = http.createServer(function(req, res){ | ||
send(req, req.url).pipe(res); | ||
}).listen(3000); | ||
```bash | ||
$ npm install send | ||
``` | ||
Serving from a root directory with custom error-handling: | ||
## API | ||
```js | ||
var http = require('http'); | ||
var send = require('send'); | ||
var url = require('url'); | ||
var app = http.createServer(function(req, res){ | ||
// your custom error-handling logic: | ||
function error(err) { | ||
res.statusCode = err.status || 500; | ||
res.end(err.message); | ||
} | ||
// your custom headers | ||
function headers(res, path, stat) { | ||
// serve all files for download | ||
res.setHeader('Content-Disposition', 'attachment'); | ||
} | ||
// your custom directory handling logic: | ||
function redirect() { | ||
res.statusCode = 301; | ||
res.setHeader('Location', req.url + '/'); | ||
res.end('Redirecting to ' + req.url + '/'); | ||
} | ||
// transfer arbitrary files from within | ||
// /www/example.com/public/* | ||
send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'}) | ||
.on('error', error) | ||
.on('directory', redirect) | ||
.on('headers', headers) | ||
.pipe(res); | ||
}).listen(3000); | ||
var send = require('send') | ||
``` | ||
## API | ||
### send(req, path, [options]) | ||
### Options | ||
Create a new `SendStream` for the given path to send to a `res`. The `req` is | ||
the Node.js HTTP request and the `path` is a urlencoded path to send (urlencoded, | ||
not the actual file-system path). | ||
#### etag | ||
#### Options | ||
Enable or disable etag generation, defaults to true. | ||
##### dotfiles | ||
#### dotfiles | ||
Set how "dotfiles" are treated when encountered. A dotfile is a file | ||
@@ -89,4 +48,8 @@ or directory that begins with a dot ("."). Note this check is done on | ||
#### extensions | ||
##### etag | ||
Enable or disable etag generation, defaults to true. | ||
##### extensions | ||
If a given file doesn't exist, try appending one of the given extensions, | ||
@@ -97,3 +60,3 @@ in the given order. By default, this is disabled (set to `false`). An | ||
#### index | ||
##### index | ||
@@ -104,4 +67,9 @@ By default send supports "index.html" files, to disable this | ||
#### maxAge | ||
##### lastModified | ||
Enable or disable `Last-Modified` header, defaults to true. Uses the file | ||
system's last modified value. | ||
##### maxAge | ||
Provide a max-age in milliseconds for http caching, defaults to 0. | ||
@@ -111,3 +79,3 @@ This can also be a string accepted by the | ||
#### root | ||
##### root | ||
@@ -118,2 +86,4 @@ Serve files relative to `path`. | ||
The `SendStream` is an event emitter and will emeit the following events: | ||
- `error` an error occurred `(err)` | ||
@@ -126,2 +96,7 @@ - `directory` a directory was requested | ||
### .pipe | ||
The `pipe` method is used to pipe the response into the Node.js HTTP response | ||
object, typically `send(req, path, options).pipe(res)`. | ||
## Error-handling | ||
@@ -151,25 +126,65 @@ | ||
## License | ||
## Examples | ||
(The MIT License) | ||
Small: | ||
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca> | ||
```js | ||
var http = require('http'); | ||
var send = require('send'); | ||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
'Software'), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
var app = http.createServer(function(req, res){ | ||
send(req, req.url).pipe(res); | ||
}).listen(3000); | ||
``` | ||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
Serving from a root directory with custom error-handling: | ||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | ||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | ||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | ||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
```js | ||
var http = require('http'); | ||
var send = require('send'); | ||
var url = require('url'); | ||
var app = http.createServer(function(req, res){ | ||
// your custom error-handling logic: | ||
function error(err) { | ||
res.statusCode = err.status || 500; | ||
res.end(err.message); | ||
} | ||
// your custom headers | ||
function headers(res, path, stat) { | ||
// serve all files for download | ||
res.setHeader('Content-Disposition', 'attachment'); | ||
} | ||
// your custom directory handling logic: | ||
function redirect() { | ||
res.statusCode = 301; | ||
res.setHeader('Location', req.url + '/'); | ||
res.end('Redirecting to ' + req.url + '/'); | ||
} | ||
// transfer arbitrary files from within | ||
// /www/example.com/public/* | ||
send(req, url.parse(req.url).pathname, {root: '/www/example.com/public'}) | ||
.on('error', error) | ||
.on('directory', redirect) | ||
.on('headers', headers) | ||
.pipe(res); | ||
}).listen(3000); | ||
``` | ||
## License | ||
[MIT](LICENSE) | ||
[npm-image]: https://img.shields.io/npm/v/send.svg?style=flat | ||
[npm-url]: https://npmjs.org/package/send | ||
[travis-image]: https://img.shields.io/travis/visionmedia/send.svg?style=flat | ||
[travis-url]: https://travis-ci.org/visionmedia/send | ||
[coveralls-image]: https://img.shields.io/coveralls/visionmedia/send.svg?style=flat | ||
[coveralls-url]: https://coveralls.io/r/visionmedia/send?branch=master | ||
[downloads-image]: https://img.shields.io/npm/dm/send.svg?style=flat | ||
[downloads-url]: https://npmjs.org/package/send | ||
[gittip-image]: https://img.shields.io/gittip/dougwilson.svg?style=flat | ||
[gittip-url]: https://www.gittip.com/dougwilson/ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
28186
183
10
5
640
2
+ Addedetag@~1.3.0
+ Addedcrc@3.0.0(transitive)
+ Addeddebug@2.0.0(transitive)
+ Addedetag@1.3.1(transitive)
- Removeddebug@1.0.4(transitive)
Updateddebug@~2.0.0