import debug from 'debug' import { BaseHandler } from './BaseHandler' import { validateHeader } from '../validators/HeaderValidator' import type http from 'node:http' import type { ServerOptions, WithRequired } from '../types' import { DataStore, Uid, CancellationContext, ERRORS, Metadata, Upload, EVENTS } from '../utils' const log = debug('tus-node-server:handlers:post') /** * PostHandler 类用于处理 HTTP POST 请求,主要用于在 DataStore 中创建文件。 * 该类继承自 BaseHandler,并重写了部分方法以实现特定的功能。 */ export class PostHandler extends BaseHandler { // 重写 BaseHandler 中的 options 类型,确保在构造函数中设置了 namingFunction declare options: WithRequired /** * 构造函数,初始化 PostHandler 实例。 * @param store - DataStore 实例,用于存储上传的文件。 * @param options - 服务器配置选项,包含 namingFunction 等。 * @throws 如果 namingFunction 不是函数,则抛出错误。 */ constructor(store: DataStore, options: ServerOptions) { if (options.namingFunction && typeof options.namingFunction !== 'function') { throw new Error("'namingFunction' must be a function") } if (!options.namingFunction) { options.namingFunction = Uid.rand } super(store, options) } /** * 在 DataStore 中创建文件。 * @param req - HTTP 请求对象。 * @param res - HTTP 响应对象。 * @param context - 取消操作的上下文。 * @returns 返回处理后的 HTTP 响应对象。 * @throws 如果请求头中包含 'upload-concat' 但 DataStore 不支持 'concatentation' 扩展,则抛出错误。 * @throws 如果请求头中 'upload-length' 和 'upload-defer-length' 同时存在或同时不存在,则抛出错误。 * @throws 如果 'upload-metadata' 解析失败,则抛出错误。 * @throws 如果文件大小超过配置的最大值,则抛出错误。 */ async send( req: http.IncomingMessage, res: http.ServerResponse, context: CancellationContext ) { if ('upload-concat' in req.headers && !this.store.hasExtension('concatentation')) { throw ERRORS.UNSUPPORTED_CONCATENATION_EXTENSION } const upload_length = req.headers['upload-length'] as string | undefined const upload_defer_length = req.headers['upload-defer-length'] as string | undefined const upload_metadata = req.headers['upload-metadata'] as string | undefined if ( upload_defer_length !== undefined && // 如果扩展不支持,则抛出错误 !this.store.hasExtension('creation-defer-length') ) { throw ERRORS.UNSUPPORTED_CREATION_DEFER_LENGTH_EXTENSION } if ((upload_length === undefined) === (upload_defer_length === undefined)) { throw ERRORS.INVALID_LENGTH } let metadata: ReturnType<(typeof Metadata)['parse']> | undefined if ('upload-metadata' in req.headers) { try { metadata = Metadata.parse(upload_metadata) } catch { throw ERRORS.INVALID_METADATA } } let id: string try { id = await this.options.namingFunction(req, metadata) } catch (error) { log('create: check your `namingFunction`. Error', error) throw error } const maxFileSize = await this.getConfiguredMaxSize(req, id) if ( upload_length && maxFileSize > 0 && Number.parseInt(upload_length, 10) > maxFileSize ) { throw ERRORS.ERR_MAX_SIZE_EXCEEDED } if (this.options.onIncomingRequest) { await this.options.onIncomingRequest(req, res, id) } const upload = new Upload({ id, size: upload_length ? Number.parseInt(upload_length, 10) : undefined, offset: 0, metadata, }) if (this.options.onUploadCreate) { try { const resOrObject = await this.options.onUploadCreate(req, res, upload) // 向后兼容,将在下一个主要版本中移除 // 由于在测试中模拟了实例,因此无法使用 `instanceof` 进行检查 if ( typeof (resOrObject as http.ServerResponse).write === 'function' && typeof (resOrObject as http.ServerResponse).writeHead === 'function' ) { res = resOrObject as http.ServerResponse } else { // 由于 TS 只理解 instanceof,因此类型定义较为丑陋 type ExcludeServerResponse = T extends http.ServerResponse ? never : T const obj = resOrObject as ExcludeServerResponse res = obj.res if (obj.metadata) { upload.metadata = obj.metadata } } } catch (error: any) { log(`onUploadCreate error: ${error.body}`) throw error } } const lock = await this.acquireLock(req, id, context) let isFinal: boolean let url: string // 推荐的响应默认值 const responseData = { status: 201, headers: {} as Record, body: '', } try { await this.store.create(upload) url = this.generateUrl(req, upload.id) this.emit(EVENTS.POST_CREATE, req, res, upload, url) isFinal = upload.size === 0 && !upload.sizeIsDeferred // 如果请求中包含 Content-Type 头,并且使用了 creation-with-upload 扩展 if (validateHeader('content-type', req.headers['content-type'])) { const bodyMaxSize = await this.calculateMaxBodySize(req, upload, maxFileSize) const newOffset = await this.writeToStore(req, upload, bodyMaxSize, context) responseData.headers['Upload-Offset'] = newOffset.toString() isFinal = newOffset === Number.parseInt(upload_length as string, 10) upload.offset = newOffset } } catch (e) { context.abort() throw e } finally { await lock.unlock() } // 上传完成后的处理逻辑 if (isFinal && this.options.onUploadFinish) { try { // 调用自定义的上传完成回调函数,传入请求、响应和上传对象 // 允许用户自定义上传完成后的处理逻辑 const resOrObject = await this.options.onUploadFinish(req, res, upload) // 兼容性处理:检查返回值是否为 HTTP 响应对象 // 通过检查对象是否具有 write 和 writeHead 方法来判断 if ( typeof (resOrObject as http.ServerResponse).write === 'function' && typeof (resOrObject as http.ServerResponse).writeHead === 'function' ) { // 如果直接返回 HTTP 响应对象,直接覆盖原响应对象 res = resOrObject as http.ServerResponse } else { // 处理自定义返回对象的情况 // 使用复杂的类型定义排除 ServerResponse 类型 type ExcludeServerResponse = T extends http.ServerResponse ? never : T // 将返回对象转换为非 ServerResponse 类型 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 } } // Upload-Expires 响应头指示未完成的上传何时过期。 // 如果在创建时已知过期时间,则必须在响应中包含 Upload-Expires 头 if ( this.store.hasExtension('expiration') && this.store.getExpiration() > 0 && upload.creation_date ) { const created = await this.store.getUpload(upload.id) if (created.offset !== Number.parseInt(upload_length as string, 10)) { const creation = new Date(upload.creation_date) // 值必须为 RFC 7231 日期时间格式 responseData.headers['Upload-Expires'] = new Date( creation.getTime() + this.store.getExpiration() ).toUTCString() } } // 仅在最终的 HTTP 状态码为 201 或 3xx 时附加 Location 头 if ( responseData.status === 201 || (responseData.status >= 300 && responseData.status < 400) ) { responseData.headers.Location = url } const writtenRes = this.write( res, responseData.status, responseData.headers, responseData.body ) if (isFinal) { this.emit(EVENTS.POST_FINISH, req, writtenRes, upload) } return writtenRes } }