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