/** * 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, 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 extends http.ServerResponse ? never : T // 将返回对象转换为自定义响应对象 const obj = resOrObject as ExcludeServerResponse // 更新响应对象 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 } } }