index.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. /**
  2. * Module dependencies.
  3. */
  4. var debug = require('debug')('koa-send');
  5. var resolvePath = require('resolve-path');
  6. var assert = require('assert');
  7. var path = require('path');
  8. var normalize = path.normalize;
  9. var basename = path.basename;
  10. var extname = path.extname;
  11. var resolve = path.resolve;
  12. var fs = require('mz/fs');
  13. /**
  14. * Expose `send()`.
  15. */
  16. module.exports = send;
  17. /**
  18. * Send file at `path` with the
  19. * given `options` to the koa `ctx`.
  20. *
  21. * @param {Context} ctx
  22. * @param {String} path
  23. * @param {Object} [opts]
  24. * @return {Function}
  25. * @api public
  26. */
  27. function send(ctx, path, opts) {
  28. assert(ctx, 'koa context required');
  29. assert(path, 'pathname required');
  30. opts = opts || {};
  31. // options
  32. debug('send "%s" %j', path, opts);
  33. var root = opts.root ? normalize(resolve(opts.root)) : '';
  34. var trailingSlash = '/' == path[path.length - 1];
  35. path = path[0] == '/' ? path.slice(1) : path;
  36. var index = opts.index;
  37. var maxage = opts.maxage || opts.maxAge || 0;
  38. var hidden = opts.hidden || false;
  39. var gzip = opts.gzip || opts.gzip === undefined ? true : false;
  40. return function *(){
  41. var encoding = this.acceptsEncodings('gzip', 'deflate', 'identity');
  42. // normalize path
  43. path = decode(path);
  44. if (-1 == path) return ctx.throw('failed to decode', 400);
  45. // index file support
  46. if (index && trailingSlash) path += index;
  47. path = resolvePath(root, path);
  48. // hidden file support, ignore
  49. if (!hidden && isHidden(root, path)) return;
  50. // serve gzipped file when possible
  51. if (encoding === 'gzip' && gzip && (yield fs.exists(path + '.gz'))) {
  52. path = path + '.gz';
  53. ctx.set('Content-Encoding', 'gzip');
  54. ctx.res.removeHeader('Content-Length');
  55. }
  56. // stat
  57. try {
  58. var stats = yield fs.stat(path);
  59. // Format the path to serve static file servers
  60. // and not require a trailing slash for directories,
  61. // so that you can do both `/directory` and `/directory/`
  62. if (stats.isDirectory()) {
  63. if (opts.format && index) {
  64. path += '/' + index;
  65. stats = yield fs.stat(path);
  66. } else {
  67. return;
  68. }
  69. }
  70. } catch (err) {
  71. var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
  72. if (~notfound.indexOf(err.code)) return;
  73. err.status = 500;
  74. throw err;
  75. }
  76. // stream
  77. ctx.set('Last-Modified', stats.mtime.toUTCString());
  78. ctx.set('Content-Length', stats.size);
  79. ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0));
  80. ctx.type = type(path);
  81. ctx.body = fs.createReadStream(path);
  82. return path;
  83. }
  84. }
  85. /**
  86. * Check if it's hidden.
  87. */
  88. function isHidden(root, path) {
  89. path = path.substr(root.length).split('/');
  90. for(var i = 0; i < path.length; i++) {
  91. if(path[i][0] === '.') return true;
  92. }
  93. return false;
  94. }
  95. /**
  96. * File type.
  97. */
  98. function type(file) {
  99. return extname(basename(file, '.gz'));
  100. }
  101. /**
  102. * Decode `path`.
  103. */
  104. function decode(path) {
  105. try {
  106. return decodeURIComponent(path);
  107. } catch (err) {
  108. return -1;
  109. }
  110. }