/** * Module dependencies. */ var debug = require('debug')('koa-send'); var resolvePath = require('resolve-path'); var assert = require('assert'); var path = require('path'); var normalize = path.normalize; var basename = path.basename; var extname = path.extname; var resolve = path.resolve; var parse = path.parse; var sep = path.sep; var fs = require('mz/fs'); var co = require('co'); /** * Expose `send()`. */ module.exports = send; /** * Send file at `path` with the * given `options` to the koa `ctx`. * * @param {Context} ctx * @param {String} path * @param {Object} [opts] * @return {Function} * @api public */ function send(ctx, path, opts) { return co(function *(){ assert(ctx, 'koa context required'); assert(path, 'pathname required'); opts = opts || {}; // options debug('send "%s" %j', path, opts); var root = opts.root ? normalize(resolve(opts.root)) : ''; var trailingSlash = '/' == path[path.length - 1]; path = path.substr(parse(path).root.length); var index = opts.index; var maxage = opts.maxage || opts.maxAge || 0; var hidden = opts.hidden || false; var format = opts.format === false ? false : true; var extensions = Array.isArray(opts.extensions) ? opts.extensions : false; var gzip = opts.gzip === false ? false : true; var setHeaders = opts.setHeaders; if (setHeaders && typeof setHeaders !== 'function') { throw new TypeError('option setHeaders must be function') } var encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity'); // normalize path path = decode(path); if (-1 == path) return ctx.throw('failed to decode', 400); // index file support if (index && trailingSlash) path += index; path = resolvePath(root, path); // hidden file support, ignore if (!hidden && isHidden(root, path)) return; // serve gzipped file when possible if (encoding === 'gzip' && gzip && (yield fs.exists(path + '.gz'))) { path = path + '.gz'; ctx.set('Content-Encoding', 'gzip'); ctx.res.removeHeader('Content-Length'); } if (extensions && !/\..*$/.exec(path)) { var list = [].concat(extensions); for (var i = 0; i < list.length; i++) { var ext = list[i]; if (typeof ext !== 'string') { throw new TypeError('option extensions must be array of strings or false'); } if (!/^\./.exec(ext)) ext = '.' + ext; if (yield fs.exists(path + ext)) { path = path + ext; break; } } } // stat try { var stats = yield fs.stat(path); // Format the path to serve static file servers // and not require a trailing slash for directories, // so that you can do both `/directory` and `/directory/` if (stats.isDirectory()) { if (format && index) { path += '/' + index; stats = yield fs.stat(path); } else { return; } } } catch (err) { var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; if (~notfound.indexOf(err.code)) return; err.status = 500; throw err; } if (setHeaders) setHeaders(ctx.res, path, stats); // stream ctx.set('Content-Length', stats.size); if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString()); if (!ctx.response.get('Cache-Control')) ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0)); ctx.type = type(path); ctx.body = fs.createReadStream(path); return path; }); } /** * Check if it's hidden. */ function isHidden(root, path) { path = path.substr(root.length).split(sep); for(var i = 0; i < path.length; i++) { if(path[i][0] === '.') return true; } return false; } /** * File type. */ function type(file) { return extname(basename(file, '.gz')); } /** * Decode `path`. */ function decode(path) { try { return decodeURIComponent(path); } catch (err) { return -1; } }