257 lines
8.1 KiB
TypeScript
257 lines
8.1 KiB
TypeScript
![]() |
/**
|
|||
|
* PATCH请求处理器模块
|
|||
|
*
|
|||
|
* 本模块负责处理TUS协议中的PATCH请求,用于上传文件的分块数据。
|
|||
|
* 主要功能包括:验证请求头、处理文件偏移量、写入数据到存储、处理上传完成事件等。
|
|||
|
*
|
|||
|
* 使用场景:
|
|||
|
* - 大文件分块上传
|
|||
|
* - 断点续传
|
|||
|
* - 文件上传进度管理
|
|||
|
*/
|
|||
|
import debug from 'debug'
|
|||
|
import { BaseHandler } from './BaseHandler'
|
|||
|
import type http from 'node:http'
|
|||
|
import { CancellationContext, ERRORS, Upload, EVENTS } from '../utils'
|
|||
|
|
|||
|
const log = debug('tus-node-server:handlers:patch')
|
|||
|
|
|||
|
/**
|
|||
|
* PATCH请求处理器类
|
|||
|
*
|
|||
|
* 继承自BaseHandler,专门处理TUS协议的PATCH请求。
|
|||
|
* 采用责任链模式,与其它处理器协同工作。
|
|||
|
*
|
|||
|
* 设计模式解析:
|
|||
|
* - 继承:扩展基础处理器功能
|
|||
|
* - 异步编程:使用async/await处理异步操作
|
|||
|
* - 事件驱动:通过EVENTS触发相关事件
|
|||
|
*
|
|||
|
* 使用示例:
|
|||
|
* const handler = new PatchHandler(store, options)
|
|||
|
* handler.send(req, res, context)
|
|||
|
*/
|
|||
|
export class PatchHandler extends BaseHandler {
|
|||
|
/**
|
|||
|
* 处理PATCH请求的核心方法
|
|||
|
*
|
|||
|
* 功能描述:
|
|||
|
* 1. 验证请求头信息
|
|||
|
* 2. 获取文件上传偏移量
|
|||
|
* 3. 写入数据到存储
|
|||
|
* 4. 处理上传完成事件
|
|||
|
* 5. 返回响应结果
|
|||
|
*
|
|||
|
* @param req HTTP请求对象
|
|||
|
* @param res HTTP响应对象
|
|||
|
* @param context 取消操作上下文
|
|||
|
* @returns 处理后的HTTP响应
|
|||
|
*
|
|||
|
* 异常处理:
|
|||
|
* - 文件未找到:抛出ERRORS.FILE_NOT_FOUND
|
|||
|
* - 偏移量缺失:抛出ERRORS.MISSING_OFFSET
|
|||
|
* - 内容类型无效:抛出ERRORS.INVALID_CONTENT_TYPE
|
|||
|
* - 文件已过期:抛出ERRORS.FILE_NO_LONGER_EXISTS
|
|||
|
* - 偏移量不匹配:抛出ERRORS.INVALID_OFFSET
|
|||
|
*/
|
|||
|
async send(
|
|||
|
req: http.IncomingMessage,
|
|||
|
res: http.ServerResponse,
|
|||
|
context: CancellationContext
|
|||
|
) {
|
|||
|
try {
|
|||
|
// 从请求中获取文件ID
|
|||
|
const id = this.getFileIdFromRequest(req)
|
|||
|
// console.log('id', id)
|
|||
|
if (!id) {
|
|||
|
throw ERRORS.FILE_NOT_FOUND
|
|||
|
}
|
|||
|
|
|||
|
// 验证Upload-Offset头是否存在
|
|||
|
if (req.headers['upload-offset'] === undefined) {
|
|||
|
throw ERRORS.MISSING_OFFSET
|
|||
|
}
|
|||
|
|
|||
|
// 解析偏移量
|
|||
|
const offset = Number.parseInt(req.headers['upload-offset'] as string, 10)
|
|||
|
|
|||
|
// 验证Content-Type头是否存在
|
|||
|
const content_type = req.headers['content-type']
|
|||
|
if (content_type === undefined) {
|
|||
|
throw ERRORS.INVALID_CONTENT_TYPE
|
|||
|
}
|
|||
|
|
|||
|
// 触发请求到达事件
|
|||
|
if (this.options.onIncomingRequest) {
|
|||
|
await this.options.onIncomingRequest(req, res, id)
|
|||
|
}
|
|||
|
|
|||
|
// 获取配置的最大文件大小
|
|||
|
const maxFileSize = await this.getConfiguredMaxSize(req, id)
|
|||
|
|
|||
|
// 获取文件锁
|
|||
|
const lock = await this.acquireLock(req, id, context)
|
|||
|
|
|||
|
let upload: Upload
|
|||
|
let newOffset: number
|
|||
|
try {
|
|||
|
// 从存储中获取上传信息
|
|||
|
upload = await this.store.getUpload(id)
|
|||
|
|
|||
|
// 检查文件是否已过期
|
|||
|
const now = Date.now()
|
|||
|
const creation = upload.creation_date
|
|||
|
? new Date(upload.creation_date).getTime()
|
|||
|
: now
|
|||
|
const expiration = creation + this.store.getExpiration()
|
|||
|
if (
|
|||
|
this.store.hasExtension('expiration') &&
|
|||
|
this.store.getExpiration() > 0 &&
|
|||
|
now > expiration
|
|||
|
) {
|
|||
|
throw ERRORS.FILE_NO_LONGER_EXISTS
|
|||
|
}
|
|||
|
|
|||
|
// 验证偏移量是否匹配
|
|||
|
if (upload.offset !== offset) {
|
|||
|
log(
|
|||
|
`[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${upload.offset}`
|
|||
|
)
|
|||
|
throw ERRORS.INVALID_OFFSET
|
|||
|
}
|
|||
|
|
|||
|
// 处理上传长度相关头信息
|
|||
|
const upload_length = req.headers['upload-length'] as string | undefined
|
|||
|
if (upload_length !== undefined) {
|
|||
|
const size = Number.parseInt(upload_length, 10)
|
|||
|
// 检查是否支持延迟长度声明
|
|||
|
if (!this.store.hasExtension('creation-defer-length')) {
|
|||
|
throw ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION
|
|||
|
}
|
|||
|
|
|||
|
// 检查上传长度是否已设置
|
|||
|
if (upload.size !== undefined) {
|
|||
|
throw ERRORS.INVALID_LENGTH
|
|||
|
}
|
|||
|
|
|||
|
// 验证长度是否有效
|
|||
|
if (size < upload.offset) {
|
|||
|
throw ERRORS.INVALID_LENGTH
|
|||
|
}
|
|||
|
|
|||
|
// 检查是否超过最大文件大小
|
|||
|
if (maxFileSize > 0 && size > maxFileSize) {
|
|||
|
throw ERRORS.ERR_MAX_SIZE_EXCEEDED
|
|||
|
}
|
|||
|
|
|||
|
// 声明上传长度
|
|||
|
await this.store.declareUploadLength(id, size)
|
|||
|
upload.size = size
|
|||
|
}
|
|||
|
|
|||
|
// 计算最大请求体大小
|
|||
|
const maxBodySize = await this.calculateMaxBodySize(req, upload, maxFileSize)
|
|||
|
// 写入数据到存储
|
|||
|
newOffset = await this.writeToStore(req, upload, maxBodySize, context)
|
|||
|
} finally {
|
|||
|
// 释放文件锁
|
|||
|
await lock.unlock()
|
|||
|
}
|
|||
|
|
|||
|
// 更新上传偏移量
|
|||
|
upload.offset = newOffset
|
|||
|
// 触发数据接收完成事件
|
|||
|
this.emit(EVENTS.POST_RECEIVE, req, res, upload)
|
|||
|
|
|||
|
// 构建响应数据
|
|||
|
const responseData = {
|
|||
|
status: 204,
|
|||
|
headers: {
|
|||
|
'Upload-Offset': newOffset,
|
|||
|
} as Record<string, string | number>,
|
|||
|
body: '',
|
|||
|
}
|
|||
|
|
|||
|
// 处理上传完成事件
|
|||
|
// 文件上传完成后的处理逻辑块
|
|||
|
if (newOffset === upload.size && this.options.onUploadFinish) {
|
|||
|
try {
|
|||
|
// 调用上传完成回调函数,支持异步处理
|
|||
|
// 允许用户自定义上传完成后的处理逻辑
|
|||
|
const resOrObject = await this.options.onUploadFinish(req, res, upload)
|
|||
|
|
|||
|
// 兼容性处理:支持两种返回类型
|
|||
|
// 1. 直接返回 http.ServerResponse 对象
|
|||
|
// 2. 返回包含自定义响应信息的对象
|
|||
|
if (
|
|||
|
// 检查是否为标准 ServerResponse 对象
|
|||
|
typeof (resOrObject as http.ServerResponse).write === 'function' &&
|
|||
|
typeof (resOrObject as http.ServerResponse).writeHead === 'function'
|
|||
|
) {
|
|||
|
// 直接使用返回的服务器响应对象
|
|||
|
res = resOrObject as http.ServerResponse
|
|||
|
} else {
|
|||
|
// 处理自定义响应对象的类型定义
|
|||
|
// 排除 ServerResponse 类型,确保类型安全
|
|||
|
type ExcludeServerResponse<T> = T extends http.ServerResponse ? never : T
|
|||
|
|
|||
|
// 将返回对象转换为自定义响应对象
|
|||
|
const obj = resOrObject as ExcludeServerResponse<typeof resOrObject>
|
|||
|
|
|||
|
// 更新响应对象
|
|||
|
res = obj.res
|
|||
|
|
|||
|
// 可选地更新响应状态码
|
|||
|
if (obj.status_code) responseData.status = obj.status_code
|
|||
|
|
|||
|
// 可选地更新响应体
|
|||
|
if (obj.body) responseData.body = obj.body
|
|||
|
|
|||
|
// 合并响应头,允许覆盖默认头
|
|||
|
if (obj.headers)
|
|||
|
responseData.headers = Object.assign(obj.headers, responseData.headers)
|
|||
|
}
|
|||
|
} catch (error: any) {
|
|||
|
// 错误处理:记录上传完成回调中的错误
|
|||
|
// 使用日志记录错误信息,并重新抛出异常
|
|||
|
log(`onUploadFinish: ${error.body}`)
|
|||
|
throw error
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 处理文件过期时间
|
|||
|
if (
|
|||
|
this.store.hasExtension('expiration') &&
|
|||
|
this.store.getExpiration() > 0 &&
|
|||
|
upload.creation_date &&
|
|||
|
(upload.size === undefined || newOffset < upload.size)
|
|||
|
) {
|
|||
|
const creation = new Date(upload.creation_date)
|
|||
|
const dateString = new Date(
|
|||
|
creation.getTime() + this.store.getExpiration()
|
|||
|
).toUTCString()
|
|||
|
responseData.headers['Upload-Expires'] = dateString
|
|||
|
}
|
|||
|
|
|||
|
// 发送响应
|
|||
|
const writtenRes = this.write(
|
|||
|
res,
|
|||
|
responseData.status,
|
|||
|
responseData.headers,
|
|||
|
responseData.body
|
|||
|
)
|
|||
|
|
|||
|
// 触发上传完成事件
|
|||
|
if (newOffset === upload.size) {
|
|||
|
this.emit(EVENTS.POST_FINISH, req, writtenRes, upload)
|
|||
|
}
|
|||
|
|
|||
|
return writtenRes
|
|||
|
} catch (e) {
|
|||
|
// 取消操作
|
|||
|
context.abort()
|
|||
|
throw e
|
|||
|
}
|
|||
|
}
|
|||
|
}
|