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, '\\"')}"`
|
||
}
|
||
}
|