return function (src, filename, done) { const relativePath = path.join(outDir, filename); const modulePath = pathResolve(cwd, relativePath); const moduleSourcePath = pathResolve(cwd, relativePath.replace(outDir, sourceDir)); const moduleBinPath = pathResolve(cwd, relativePath.replace(outDir, binDir)); const pathOptions = { moduleBinPath, modulePath, moduleSourcePath, relativePath }; const origin = generalSettings.staticUrl; return new Promise((resolveImportable, rejectImportable) => { createImportableModule({ origin, pathOptions, logger, logLevel: 0, temporaryCache: false, binaryToModule, root: cwd }, resolveImportable, rejectImportable); }) .then(() => done && done()) .catch((err) => done && done(err)); };
return function *serve(next){ if (this.method == 'HEAD' || this.method == 'GET') { var path = this.path.substr(parse(this.path).root.length); try { path = decodeURIComponent(path); } catch (ex) { // 无效路径 this.status = 404; this.body = "could not decode path"; return; } if (options.index && '/' == this.path[this.path.length - 1]) path += options.index; path = resolvePath(normalizedRoot, path); if (!options.hidden && isHidden(root, path)) return; if (yield send(this, path, options)) return; } yield* next; };
// utility function* send(ctx, path) { var req = ctx.request var res = ctx.response path = path || req.path.slice(1) || '' // index file support var directory = path === '' || path.slice(-1) === '/' if (index && directory) path += 'index.html' // regular paths can not be absolute path = resolve(root, path) // hidden file support if (!hidden && leadingDot(path)) return var file = yield* get(path) if (!file) return // 404 // proper method handling var method = req.method switch (method) { case 'HEAD': case 'GET': break // continue case 'OPTIONS': res.set('Allow', methods) res.status = 204 return file default: res.set('Allow', methods) res.status = 405 return file } res.status = 200 res.etag = file.etag res.lastModified = file.stats.mtime res.type = file.type if (cachecontrol) res.set('Cache-Control', cachecontrol) if (req.fresh) { res.status = 304 return file } if (method === 'HEAD') return file if (file.compress && req.acceptsEncodings('gzip', 'identity') === 'gzip') { res.set('Content-Encoding', 'gzip') res.length = file.compress.stats.size res.body = fs.createReadStream(file.compress.path) } else { res.set('Content-Encoding', 'identity') res.length = file.stats.size res.body = fs.createReadStream(path) } return file }
.then(function( stats ){ if (stats.isDirectory() && $preprocessor.index) { fpath = resolvePath( fpath , $preprocessor.index ); return getStats(); } return stats; });
return function middleware( req , res , next ){ var pathname = url.parse( req.url ).pathname; var fpath = resolvePath( $preprocessor.root , pathname.substr( 1 )); Q.fcall(function(){ if (req.method != 'GET') { return Q.reject(); } }) .then(function(){ return Q.fcall(function getStats(){ return Q.promise(function( resolve , reject ){ fs.stat( fpath , function( err , stats ){ return err ? reject() : resolve( stats ); }); }) .then(function( stats ){ if (stats.isDirectory() && $preprocessor.index) { fpath = resolvePath( fpath , $preprocessor.index ); return getStats(); } return stats; }); }) .then(function( stats ){ if (stats.isFile() && $preprocessor._accepts( fpath )) { return Q.promise(function( resolve , reject ){ fs.readFile( fpath , 'utf-8' , function( err , content ){ return err ? reject( err ) : resolve( content ); }); }); } return Q.reject(); }); }) .then(function( content ){ var $req = _.extend(Object.create( req ), { root: fpath.substr( 0 , fpath.indexOf( pathname )), pathname: fpath.substr(fpath.indexOf( pathname )), mime: mime.lookup( fpath ) }); return Q.resolve().then(function(){ return $preprocessor.engine( content , $req ); }) .then(function( processed ){ res.setHeader( 'Content-Type' , $req.mime ); res.setHeader( 'Content-Length' , Buffer.byteLength( processed )); return processed; }); }) .then(function( content ){ res.end( content ); }) .fail( next ); };
return function *(){ var trailingSlash = '/' == path[path.length - 1]; var encoding = this.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 && leadingDot(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'); } // stat try { var stats = yield fs.stat(path); if (stats.isDirectory()) return; } catch (err) { var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']; if (~notfound.indexOf(err.code)) return; err.status = 500; throw err; } ctx.set('Last-Modified', stats.mtime.toUTCString()); ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0)); // respond 304 if mtime not newer var clientMtime = new Date(ctx.request.get('If-Modified-Since')); if (clientMtime.getTime() === clientMtime.getTime()) { // not NaN if (!(stats.mtime > clientMtime)) { ctx.response.status = 304; return path; } } // stream ctx.set('Content-Length', stats.size); ctx.type = type(path); ctx.body = fs.createReadStream(path); return path; }
function* push(ctx, path, opts) { assert(path, 'you must define a path!') if (!ctx.res.isSpdy) return opts = opts || {} assert(path[0] !== '/', 'you can only push relative paths') var uri = path // original path // index file support var directory = path === '' || path.slice(-1) === '/' if (index && directory) path += 'index.html' // regular paths can not be absolute path = resolve(root, path) var file = yield* get(path) assert(file, 'can not push file: ' + uri) var options = { path: '/' + uri, priority: opts.priority, } var headers = options.headers = { 'content-type': file.type, etag: file.etag, 'last-modified': file.stats.mtime.toUTCString(), } if (cachecontrol) headers['cache-control'] = cachecontrol if (file.compress) { headers['content-encoding'] = 'gzip' headers['content-length'] = file.compress.stats.size options.filename = file.compress.path } else { headers['content-encoding'] = 'identity' headers['content-length'] = file.stats.size options.filename = path } spdy(ctx.res) .push(options) .send() .catch(ctx.onerror) return file }
async function send (ctx, path, opts = {}) { assert(ctx, 'koa context required') assert(path, 'pathname required') // options debug('send "%s" %j', path, opts) const root = opts.root ? normalize(resolve(opts.root)) : '' const trailingSlash = path[path.length - 1] === '/' path = path.substr(parse(path).root.length) const index = opts.index const maxage = opts.maxage || opts.maxAge || 0 const immutable = opts.immutable || false const hidden = opts.hidden || false const format = opts.format !== false const extensions = Array.isArray(opts.extensions) ? opts.extensions : false const brotli = opts.brotli !== false const gzip = opts.gzip !== false const setHeaders = opts.setHeaders if (setHeaders && typeof setHeaders !== 'function') { throw new TypeError('option setHeaders must be function') } // normalize path path = decode(path) if (path === -1) return ctx.throw(400, 'failed to decode') // index file support if (index && trailingSlash) path += index path = resolvePath(root, path) // hidden file support, ignore if (!hidden && isHidden(root, path)) return let encodingExt = '' // serve brotli file when possible otherwise gzipped file when possible if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await fs.exists(path + '.br'))) { path = path + '.br' ctx.set('Content-Encoding', 'br') ctx.res.removeHeader('Content-Length') encodingExt = '.br' } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (await fs.exists(path + '.gz'))) { path = path + '.gz' ctx.set('Content-Encoding', 'gzip') ctx.res.removeHeader('Content-Length') encodingExt = '.gz' } if (extensions && !/\.[^/]*$/.exec(path)) { const list = [].concat(extensions) for (let i = 0; i < list.length; i++) { let 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 (await fs.exists(path + ext)) { path = path + ext break } } } // stat let stats try { stats = await 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 = await fs.stat(path) } else { return } } } catch (err) { const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'] if (notfound.includes(err.code)) { throw createError(404, err) } 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')) { const directives = ['max-age=' + (maxage / 1000 | 0)] if (immutable) { directives.push('immutable') } ctx.set('Cache-Control', directives.join(',')) } if (!ctx.type) ctx.type = type(path, encodingExt) ctx.body = fs.createReadStream(path) return path }