225 lines · 5.6 KB · JavaScript
RawDownload
 /*! * Connect - staticProvider * Copyright(c) 2010 Sencha Inc. * Copyright(c) 2011 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var fs = require('fs')  , path = require('path')  , join = path.join  , basename = path.basename  , normalize = path.normalize  , utils = require('../utils')  , Buffer = require('buffer').Buffer  , parse = require('url').parse  , mime = require('mime'); /** * Static file server with the given `root` path. * * Examples: * *     var oneDay = 86400000; * *     connect( *       connect.static(__dirname + '/public') *     ).listen(3000); * *     connect( *       connect.static(__dirname + '/public', { maxAge: oneDay }) *     ).listen(3000); * * Options: * *    - `maxAge`   Browser cache maxAge in milliseconds. defaults to 0 *    - `hidden`   Allow transfer of hidden files. defaults to false *    - `redirect`   Redirect to trailing "/" when the pathname is a dir * * @param {String} root * @param {Object} options * @return {Function} * @api public */ exports = module.exports = function static(root, options){  options = options || {};   // root required  if (!root) throw new Error('static() root path required');  options.root = root;   return function static(req, res, next) {    options.path = req.url;    options.getOnly = true;    send(req, res, next, options);  };}; /** * Expose mime module. */ exports.mime = mime; /** * Respond with 416  "Requested Range Not Satisfiable" * * @param {ServerResponse} res * @api private */ function invalidRange(res) {  var body = 'Requested Range Not Satisfiable';  res.setHeader('Content-Type', 'text/plain');  res.setHeader('Content-Length', body.length);  res.statusCode = 416;  res.end(body);} /** * Attempt to tranfer the requseted 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;   // 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 = decodeURIComponent(url.pathname)    , type;   // null byte(s)  if (~path.indexOf('\0')) return utils.badRequest(res);   // when root is not given, consider .. malicious  if (!root && ~path.indexOf('..')) return utils.forbidden(res);   // join / normalize from optional root dir  path = normalize(join(root, path));   // malicious path  if (root && 0 != path.indexOf(root)) return fn    ? fn(new Error('Forbidden'))    : utils.forbidden(res);   // index.html support  if (normalize('/') == path[path.length - 1]) path += 'index.html';   // "hidden" file  if (!hidden && '.' == basename(path)[0]) return next();   fs.stat(path, function(err, stat){    // mime type    type = mime.lookup(path);     // ignore ENOENT    if (err) {      if (fn) return fn(err);      return 'ENOENT' == err.code        ? next()        : next(err);    // redirect directory in case index.html is present    } else if (stat.isDirectory()) {      if (!redirect) return next();      res.statusCode = 301;      res.setHeader('Location', url.pathname + '/');      res.end('Redirecting to ' + url.pathname + '/');      return;    }     // header fields    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('ETag')) res.setHeader('ETag', utils.etag(stat));    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)) {      if (!utils.modified(req, res)) {        req.emit('static');        return utils.notModified(res);      }    }     var opts = {};    var chunkSize = stat.size;     // we have a Range request    if (ranges) {      ranges = utils.parseRange(stat.size, ranges);      // valid      if (ranges) {        // TODO: stream options        // TODO: multiple support        opts.start = ranges[0].start;        opts.end = ranges[0].end;        chunkSize = opts.end - opts.start + 1;        res.statusCode = 206;        res.setHeader('Content-Range', 'bytes '          + opts.start          + '-'          + opts.end          + '/'          + stat.size);      // invalid      } else {        return fn          ? fn(new Error('Requested Range Not Satisfiable'))          : invalidRange(res);      }    }     res.setHeader('Content-Length', chunkSize);     // transfer    if (head) return res.end();     // stream    var stream = fs.createReadStream(path, opts);    req.emit('static', stream);    stream.pipe(res);     // callback    if (fn) {      function callback(err) { done || fn(err); done = true }      req.on('close', callback);      stream.on('end', callback);    }  });};