doctor-mail/packages/tus/src/handlers/PostHandler.ts

257 lines
8.7 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.

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<ServerOptions, 'namingFunction'>
/**
* 构造函数,初始化 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> = T extends http.ServerResponse ? never : T
const obj = resOrObject as ExcludeServerResponse<typeof resOrObject>
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<string, string | number>,
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> = T extends http.ServerResponse ? never : T
// 将返回对象转换为非 ServerResponse 类型
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
}
}
// 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
}
}