You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

185 lines
5.0 KiB

/**
* 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<boolean>} 文件是否存在
*/
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;