Add res.sendFile

fixes #1906
closes #2266
This commit is contained in:
Douglas Christopher Wilson
2014-08-06 02:09:25 -04:00
parent 7e32fa1be6
commit 2cb029f896
3 changed files with 280 additions and 0 deletions

View File

@@ -1,6 +1,9 @@
unreleased
==========
* add `res.sendFile`
- accepts a file system path instead of a URL
- requires an absolute path or `root` option specified
* deps: qs@1.0.2
- Complete rewrite
- Limits array length to 20

View File

@@ -5,6 +5,7 @@
var deprecate = require('depd')('express');
var escapeHtml = require('escape-html');
var http = require('http');
var isAbsolute = require('./utils').isAbsolute;
var path = require('path');
var mixin = require('utils-merge');
var sign = require('cookie-signature').sign;
@@ -301,6 +302,123 @@ res.jsonp = function jsonp(obj) {
return this.send(body);
};
/**
* Transfer the file at the given `path`.
*
* Automatically sets the _Content-Type_ response header field.
* The callback `fn(err)` is invoked when the transfer is complete
* or when an error occurs. Be sure to check `res.sentHeader`
* if you wish to attempt responding, as the header and some data
* may have already been transferred.
*
* Options:
*
* - `maxAge` defaulting to 0 (can be string converted by `ms`)
* - `root` root directory for relative filenames
* - `headers` object of headers to serve with file
* - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
*
* Other options are passed along to `send`.
*
* Examples:
*
* The following example illustrates how `res.sendFile()` may
* be used as an alternative for the `static()` middleware for
* dynamic situations. The code backing `res.sendFile()` is actually
* the same code, so HTTP cache support etc is identical.
*
* app.get('/user/:uid/photos/:file', function(req, res){
* var uid = req.params.uid
* , file = req.params.file;
*
* req.user.mayViewFilesFrom(uid, function(yes){
* if (yes) {
* res.sendFile('/uploads/' + uid + '/' + file);
* } else {
* res.send(403, 'Sorry! you cant see that.');
* }
* });
* });
*
* @api public
*/
res.sendFile = function sendFile(path, options, fn) {
var done;
var req = this.req;
var next = req.next;
if (!path) {
throw new TypeError('path argument is required to res.sendFile');
}
// support function as second arg
if (typeof options === 'function') {
fn = options;
options = {};
}
options = options || {};
if (!options.root && !isAbsolute(path)) {
throw new TypeError('path must be absolute or specify root to res.sendFile');
}
// socket errors
req.socket.on('error', onerror);
// errors
function onerror(err) {
if (done) return;
done = true;
// clean up
cleanup();
// callback available
if (fn) return fn(err);
// delegate
next(err);
}
// streaming
function onstream(stream) {
if (done) return;
cleanup();
if (fn) stream.on('end', fn);
}
// cleanup
function cleanup() {
req.socket.removeListener('error', onerror);
}
// transfer
var pathname = encodeURI(path);
var file = send(req, pathname, options);
file.on('error', onerror);
file.on('directory', next);
file.on('stream', onstream);
if (options.headers) {
// set headers on successful transfer
file.on('headers', function headers(res) {
var obj = options.headers;
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
res.setHeader(k, obj[k]);
}
});
}
// pipe
file.pipe(this);
this.on('finish', cleanup);
};
/**
* Transfer the file at the given `path`.
*

View File

@@ -1,9 +1,158 @@
var after = require('after');
var express = require('../')
, request = require('supertest')
, assert = require('assert');
var path = require('path');
var should = require('should');
var fixtures = path.join(__dirname, 'fixtures');
describe('res', function(){
describe('.sendFile(path)', function () {
it('should transfer a file', function (done) {
var app = createApp(path.resolve(fixtures, 'name.txt'));
request(app)
.get('/')
.expect(200, 'tobi', done);
});
it('should transfer a file with special characters in string', function (done) {
var app = createApp(path.resolve(fixtures, '% of dogs.txt'));
request(app)
.get('/')
.expect(200, '20%', done);
});
it('should 404 when not found', function (done) {
var app = createApp(path.resolve(fixtures, 'does-no-exist'));
app.use(function (req, res) {
res.statusCode = 200;
res.send('no!');
});
request(app)
.get('/')
.expect(404, done);
});
it('should not override manual content-types', function (done) {
var app = express();
app.use(function (req, res) {
res.contentType('application/x-bogus');
res.sendFile(path.resolve(fixtures, 'name.txt'));
});
request(app)
.get('/')
.expect('Content-Type', 'application/x-bogus')
.end(done);
})
describe('with "dotfiles" option', function () {
it('should not serve dotfiles by default', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/.name'));
request(app)
.get('/')
.expect(404, done);
});
it('should accept dotfiles option', function(done){
var app = createApp(path.resolve(__dirname, 'fixtures/.name'), { dotfiles: 'allow' });
request(app)
.get('/')
.expect(200, 'tobi', done);
});
});
describe('with "headers" option', function () {
it('should accept headers option', function (done) {
var headers = {
'x-success': 'sent',
'x-other': 'done'
};
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { headers: headers });
request(app)
.get('/')
.expect('x-success', 'sent')
.expect('x-other', 'done')
.expect(200, done);
});
it('should ignore headers option on 404', function (done) {
var headers = { 'x-success': 'sent' };
var app = createApp(path.resolve(__dirname, 'fixtures/does-not-exist'), { headers: headers });
request(app)
.get('/')
.expect(404, function (err, res) {
if (err) return done(err);
res.headers.should.not.have.property('x-success');
done();
});
});
});
describe('with "root" option', function () {
it('should not transfer relative with without', function (done) {
var app = createApp('test/fixtures/name.txt');
request(app)
.get('/')
.expect(500, /must be absolute/, done);
})
it('should serve relative to "root"', function (done) {
var app = createApp('name.txt', {root: fixtures});
request(app)
.get('/')
.expect(200, 'tobi', done);
})
it('should disallow requesting out of "root"', function (done) {
var app = createApp('foo/../../user.html', {root: fixtures});
request(app)
.get('/')
.expect(403, done);
})
})
})
describe('.sendFile(path, fn)', function () {
it('should invoke the callback when complete', function (done) {
var cb = after(2, done);
var app = createApp(path.resolve(fixtures, 'name.txt'), cb);
request(app)
.get('/')
.expect(200, cb);
})
it('should invoke the callback on 404', function(done){
var app = express();
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
should(err).be.ok;
err.status.should.equal(404);
res.send('got it');
});
});
request(app)
.get('/')
.expect(200, 'got it', done);
})
})
describe('.sendfile(path, fn)', function(){
it('should invoke the callback when complete', function(done){
var app = express();
@@ -331,3 +480,13 @@ describe('res', function(){
})
})
})
function createApp(path, options, fn) {
var app = express();
app.use(function (req, res) {
res.sendFile(path, options, fn);
});
return app;
}