Skip to content

Commit

Permalink
Replace connect.static() internals with send module
Browse files Browse the repository at this point in the history
  • Loading branch information
tj committed Jul 4, 2012
1 parent 6f67e65 commit 3ea7fa2
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 318 deletions.
230 changes: 27 additions & 203 deletions lib/middleware/static.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

/*!
* Connect - staticProvider
* Connect - static
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
Expand All @@ -9,17 +10,10 @@
* Module dependencies.
*/

var fs = require('fs')
, path = require('path')
, join = path.join
, basename = path.basename
, normalize = path.normalize
, fresh = require('fresh')
var send = require('send')
, utils = require('../utils')
, parseRange = require('range-parser')
, Buffer = require('buffer').Buffer
, parse = require('url').parse
, mime = require('mime');
, parse = utils.parseUrl
, url = require('url');

/**
* Static:
Expand Down Expand Up @@ -55,10 +49,28 @@ exports = module.exports = function static(root, options){
if (!root) throw new Error('static() root path required');
options.root = root;

// default redirect
var redirect = false === options.redirect ? false : true;

return function static(req, res, next) {
options.path = req.url;
options.getOnly = true;
send(req, res, next, options);
if ('GET' != req.method && 'HEAD' != req.method) return next();
var path = parse(req).pathname;

function directory() {
if (!redirect) return next();
var pathname = url.parse(req.originalUrl).pathname;
res.statusCode = 301;
res.setHeader('Location', pathname + '/');
res.end('Redirecting to ' + utils.escape(pathname) + '/');
}

send(path)
.maxage(options.maxAge || 0)
.root(options.root)
.hidden(options.hidden)
.on('error', next)
.on('directory', directory)
.pipe(res);
};
};

Expand All @@ -69,192 +81,4 @@ exports = module.exports = function static(root, options){
* reference to the "mime" module in the npm registry.
*/

exports.mime = mime;

/**
* 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 err;
}
}

/**
* Attempt to transfer the requested file to `res`.
*
* @param {ServerRequest}
* @param {ServerResponse}
* @param {Function} next
* @param {Object} options
* @api private
*/

var send = exports.send = function(req, res, next, options){
options = options || {};
if (!options.path) throw new Error('path required');

// setup
var maxAge = options.maxAge || 0
, ranges = req.headers.range
, head = 'HEAD' == req.method
, get = 'GET' == req.method
, root = options.root ? normalize(options.root) : null
, redirect = false === options.redirect ? false : true
, getOnly = options.getOnly
, fn = options.callback
, hidden = options.hidden
, done;

if (Infinity == maxAge) maxAge = 60 * 60 * 24 * 365 * 1000;

// replace next() with callback when available
if (fn) next = fn;

// ignore non-GET requests
if (getOnly && !get && !head) return next();

// parse url
var url = parse(options.path)
, path = decode(url.pathname)
, type;

if (path instanceof URIError) return next(utils.error(400));

// null byte(s)
if (~path.indexOf('\0')) return next(utils.error(400));

// when root is not given, consider .. malicious
if (!root && ~path.indexOf('..')) return next(utils.error(403));

// index.html support
if ('/' == path[path.length - 1]) path += 'index.html';

// join / normalize from optional root dir
path = normalize(join(root, path));

// malicious path
if (root && 0 != path.indexOf(root)) return next(utils.error(403));

// "hidden" file
if (!hidden && '.' == basename(path)[0]) return next();

fs.stat(path, function(err, stat){
// mime type
type = mime.lookup(path);

// ignore ENOENT, ENAMETOOLONG and ENOTDIR
if (err) {
if (fn) return fn(err);
return ('ENOENT' == err.code || 'ENAMETOOLONG' == err.code || 'ENOTDIR' == err.code)
? next()
: next(err);
// redirect directory in case index.html is present
} else if (stat.isDirectory()) {
if (!redirect) return next();
url = parse(req.originalUrl);
res.statusCode = 301;
res.setHeader('Location', url.pathname + '/');
res.end('Redirecting to ' + url.pathname + '/');
return;
}

// header fields
if (!res.getHeader('ETag')) res.setHeader('ETag', utils.etag(stat));
if (!res.getHeader('Date')) res.setHeader('Date', new Date().toUTCString());
if (!res.getHeader('Cache-Control')) res.setHeader('Cache-Control', 'public, max-age=' + (maxAge / 1000));
if (!res.getHeader('Last-Modified')) res.setHeader('Last-Modified', stat.mtime.toUTCString());
if (!res.getHeader('Content-Type')) {
var charset = mime.charsets.lookup(type);
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
}
res.setHeader('Accept-Ranges', 'bytes');

// conditional GET support
if (utils.conditionalGET(req)) {
// only use cache if original response would have had a
// status code of 2xx or 304 (see RFC 2616 section 14.25 and 14.26)
if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 304) {
if (fresh(req.headers, res._headers)) {
req.emit('static');
return utils.notModified(res);
}
}
}

var opts = {}
, len = stat.size;

// we have a Range request
if (ranges) {
ranges = parseRange(len, ranges);

// unsatisfiable
if (-1 == ranges) {
res.setHeader('Content-Range', 'bytes */' + stat.size);
return next(utils.error(416));
}

// valid
if (-2 != ranges) {
opts.start = ranges[0].start;
opts.end = ranges[0].end;

// Content-Range
len = opts.end - opts.start + 1;
res.statusCode = 206;
res.setHeader('Content-Range', 'bytes '
+ opts.start
+ '-'
+ opts.end
+ '/'
+ stat.size);
}
}

res.setHeader('Content-Length', len);

// transfer
if (head) return res.end();

// stream
var stream = fs.createReadStream(path, opts);
req.emit('static', stream);
req.on('close', stream.destroy.bind(stream));
stream.pipe(res);

// clean up and flag as
// done for remaining events
function callback(err) {
done || fn(err);
done = true;
req.socket.removeListener('error', callback);
}

// callback
if (fn) {
req.on('close', callback);
req.socket.on('error', callback);
stream.on('error', callback);
stream.on('end', callback);
} else {
stream.on('error', function(err){
if (res.headerSent) {
console.error(err.stack);
req.destroy();
} else {
next(err);
}
});
}
});
};
exports.mime = send.mime;
3 changes: 3 additions & 0 deletions lib/middleware/staticCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ module.exports = function staticCache(options){
, cache = new Cache(options.maxObjects || 128)
, maxlen = options.maxLength || 1024 * 256;

console.warn('connect.staticCache() is deprecated and will be removed in 3.0');
console.warn('use varnish or similar reverse proxy caches.');

return function staticCache(req, res, next){
var key = cacheKey(req)
, ranges = req.headers.range
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
"author": "TJ Holowaychuk <[email protected]> (http://tjholowaychuk.com)",
"dependencies": {
"qs": "0.4.2",
"mime": "1.2.4",
"formidable": "1.0.11",
"crc": "0.2.0",
"cookie": "0.0.4",
"fresh": "0.0.1",
"bytes": "0.0.1",
"range-parser": "0.0.4",
"send": "0.0.1",
"debug": "*"
},
"devDependencies": {
Expand Down
112 changes: 0 additions & 112 deletions test/staticCache.js

This file was deleted.

1 comment on commit 3ea7fa2

@seasoup
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey guys, how come you no longer emit a 'static' event when serving a static page? I was using this to do things whenever a static file was being served, is there a new way to be doing this? Thanks.

Please sign in to comment.