fenghuo/packages/tus/src/handlers/PatchHandler.ts

257 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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
}
}
}