190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
![]() |
/**
|
|||
|
* 文件模块: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<string,RouteHandler> = 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<stream.Writable | void> {
|
|||
|
// 检查是否注册了自定义路径处理
|
|||
|
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, '\\"')}"`
|
|||
|
}
|
|||
|
}
|