/** * 文件模块:GetHandler.ts * 功能描述:负责处理HTTP GET请求,提供文件下载和流式传输功能 * 使用场景:适用于需要实现文件下载、流媒体播放等功能的Web服务 */ import stream from 'node:stream' import { BaseHandler } from './BaseHandler' import type http from 'node:http' import type { RouteHandler } from '../types' import { ERRORS, Upload } from '../utils' /** * GetHandler类 * 核心功能:处理GET请求,支持自定义路径处理和文件流传输 * 设计模式:基于策略模式实现路径处理函数的动态注册 * 使用示例: * const handler = new GetHandler() * handler.registerPath('/custom', customHandler) */ export class GetHandler extends BaseHandler { // 使用Map存储路径与处理函数的映射关系,提供O(1)的查找时间复杂度 paths: Map = new Map() /** * 正则表达式用于验证MIME类型是否符合RFC1341规范 * 支持带参数的MIME类型,如:text/plain; charset=utf-8 * 时间复杂度:O(n),n为字符串长度 * 优化建议:可考虑预编译正则表达式以提高性能 */ reMimeType = // biome-ignore lint/suspicious/noControlCharactersInRegex: it's fine /^(?:application|audio|example|font|haptics|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*+.^_`|~-]+)((?:[ ]*;[ ]*[0-9A-Za-z!#$%&'*+.^_`|~-]+=(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+|"(?:[^"\\]|\.)*"))*)$/ /** * 允许浏览器内联渲染的MIME类型白名单 * 使用Set数据结构,提供O(1)的查找时间复杂度 * 优化建议:可根据实际业务需求动态调整白名单 */ mimeInlineBrowserWhitelist = new Set([ 'text/plain', 'image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/webp', 'audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-pn-wav', 'audio/webm', 'audio/ogg', 'video/mp4', 'video/webm', 'video/ogg', 'application/ogg', ]) /** * 注册路径处理函数 * 功能描述:将路径与处理函数进行绑定 * 输入参数: * - path: 请求路径 * - handler: 处理函数 * 时间复杂度:O(1) * 优化建议:可添加路径冲突检测机制 */ registerPath(path: string, handler: RouteHandler): void { this.paths.set(path, handler) } /** * 发送文件流 * 功能描述:处理GET请求,返回文件流或执行自定义处理 * 输入参数: * - req: HTTP请求对象 * - res: HTTP响应对象 * 返回值:可写流或void * 异常处理:抛出FILE_NOT_FOUND错误 * 时间复杂度:O(n),n为文件大小 * 优化建议:可添加流控机制防止内存溢出 */ async send( req: http.IncomingMessage, res: http.ServerResponse // biome-ignore lint/suspicious/noConfusingVoidType: it's fine ): Promise { // 检查是否注册了自定义路径处理 if (this.paths.has(req.url as string)) { const handler = this.paths.get(req.url as string) as RouteHandler return handler(req, res) } // 检查数据存储是否支持读取操作 if (!('read' in this.store)) { throw ERRORS.FILE_NOT_FOUND } // 从请求中提取文件ID const id = this.getFileIdFromRequest(req) if (!id) { throw ERRORS.FILE_NOT_FOUND } // 执行自定义请求处理回调 if (this.options.onIncomingRequest) { await this.options.onIncomingRequest(req, res, id) } // 获取文件上传状态 const stats = await this.store.getUpload(id) // 验证文件是否完整 if (!stats || stats.offset !== stats.size) { throw ERRORS.FILE_NOT_FOUND } // 处理内容类型和内容处置头 const { contentType, contentDisposition } = this.filterContentType(stats) // 创建文件读取流 // @ts-expect-error exists if supported const file_stream = await this.store.read(id) const headers = { 'Content-Length': stats.offset, 'Content-Type': contentType, 'Content-Disposition': contentDisposition, } res.writeHead(200, headers) // 使用流管道传输数据 return stream.pipeline(file_stream, res, () => { // 忽略流传输错误 }) } /** * 过滤内容类型 * 功能描述:根据文件类型生成Content-Type和Content-Disposition头 * 输入参数: * - stats: 文件上传状态对象 * 返回值:包含contentType和contentDisposition的对象 * 时间复杂度:O(1) * 优化建议:可添加更多MIME类型验证规则 */ filterContentType(stats: Upload): { contentType: string contentDisposition: string } { let contentType: string let contentDisposition: string // 从元数据中提取文件类型和名称 const { filetype, filename } = stats.metadata ?? {} // 验证文件类型格式 if (filetype && this.reMimeType.test(filetype)) { contentType = filetype // 检查是否在白名单中 if (this.mimeInlineBrowserWhitelist.has(filetype)) { contentDisposition = 'inline' } else { contentDisposition = 'attachment' } } else { // 使用默认类型并强制下载 contentType = 'application/octet-stream' contentDisposition = 'attachment' } // 添加文件名到内容处置头 if (filename) { contentDisposition += `; filename=${this.quote(filename)}` } return { contentType, contentDisposition, } } /** * 字符串转义 * 功能描述:将字符串转换为带引号的字符串字面量 * 输入参数: * - value: 需要转义的字符串 * 返回值:转义后的字符串 * 时间复杂度:O(n),n为字符串长度 * 优化建议:可考虑使用正则表达式优化替换操作 */ quote(value: string) { return `"${value.replace(/"/g, '\\"')}"` } }