/** * koa-send@5.0.1 转换为ES Module版本 * 静态资源服务中间件 */ import fs from 'fs'; import { promisify } from 'util'; import logger from 'log4js'; import resolvePath from './resolve-path.js'; import createError from 'http-errors'; import assert from 'assert'; import { normalize, basename, extname, resolve, parse, sep } from 'path'; import { fileURLToPath } from 'url'; import path from "path" // 转换为ES Module格式 const log = logger.getLogger('koa-send'); const stat = promisify(fs.stat); const access = promisify(fs.access); const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * 检查文件是否存在 * @param {string} path - 文件路径 * @returns {Promise} 文件是否存在 */ async function exists(path) { try { await access(path); return true; } catch (e) { return false; } } /** * 发送文件给客户端 * @param {Context} ctx - Koa上下文对象 * @param {String} path - 文件路径 * @param {Object} [opts] - 配置选项 * @returns {Promise} - 异步Promise */ async function send(ctx, path, opts = {}) { assert(ctx, 'koa context required'); assert(path, 'pathname required'); // 移除硬编码的public目录,要求必须通过opts.root配置 const root = opts.root; if (!root) { throw new Error('Static root directory must be configured via opts.root'); } const trailingSlash = path[path.length - 1] === '/'; path = path.substr(parse(path).root.length); const index = opts.index || 'index.html'; 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'); } // 解码路径 path = decode(path); if (path === -1) return ctx.throw(400, 'failed to decode'); // 索引文件支持 if (index && trailingSlash) path += index; path = resolvePath(root, path); // 隐藏文件支持 if (!hidden && isHidden(root, path)) return; let encodingExt = ''; // 尝试提供压缩文件 if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await 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 exists(path + '.gz'))) { path = path + '.gz'; ctx.set('Content-Encoding', 'gzip'); ctx.res.removeHeader('Content-Length'); encodingExt = '.gz'; } // 尝试添加文件扩展名 if (extensions && !/\./.exec(basename(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 exists(`${path}${ext}`)) { path = `${path}${ext}`; break; } } } // 获取文件状态 let stats; try { stats = await stat(path); // 处理目录 if (stats.isDirectory()) { if (format && index) { path += `/${index}`; stats = await 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); // 设置响应头 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; } /** * 检查是否为隐藏文件 * @param {string} root - 根目录 * @param {string} path - 文件路径 * @returns {boolean} 是否为隐藏文件 */ function isHidden(root, path) { path = path.substr(root.length).split(sep); for (let i = 0; i < path.length; i++) { if (path[i][0] === '.') return true; } return false; } /** * 获取文件类型 * @param {string} file - 文件路径 * @param {string} ext - 编码扩展名 * @returns {string} 文件MIME类型 */ function type(file, ext) { return ext !== '' ? extname(basename(file, ext)) : extname(file); } /** * 解码URL路径 * @param {string} path - 需要解码的路径 * @returns {string|number} 解码后的路径或错误代码 */ function decode(path) { try { return decodeURIComponent(path); } catch (err) { return -1; } } export default send;