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

257 lines
8.1 KiB
TypeScript
Raw Normal View History

2025-05-27 16:56:50 +08:00
/**
* 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请求处理器类
*
* BaseHandlerTUS协议的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
}
}
}