2025-01-06 08:45:23 +08:00
|
|
|
|
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
|
|
|
|
|
}
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
2025-01-06 08:45:23 +08:00
|
|
|
|
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()
|
|
|
|
|
}
|
2025-01-06 18:30:16 +08:00
|
|
|
|
// 上传完成后的处理逻辑
|
2025-01-06 08:45:23 +08:00
|
|
|
|
if (isFinal && this.options.onUploadFinish) {
|
|
|
|
|
try {
|
2025-01-06 18:30:16 +08:00
|
|
|
|
// 调用自定义的上传完成回调函数,传入请求、响应和上传对象
|
|
|
|
|
// 允许用户自定义上传完成后的处理逻辑
|
2025-01-06 08:45:23 +08:00
|
|
|
|
const resOrObject = await this.options.onUploadFinish(req, res, upload)
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 兼容性处理:检查返回值是否为 HTTP 响应对象
|
|
|
|
|
// 通过检查对象是否具有 write 和 writeHead 方法来判断
|
2025-01-06 08:45:23 +08:00
|
|
|
|
if (
|
|
|
|
|
typeof (resOrObject as http.ServerResponse).write === 'function' &&
|
|
|
|
|
typeof (resOrObject as http.ServerResponse).writeHead === 'function'
|
|
|
|
|
) {
|
2025-01-06 18:30:16 +08:00
|
|
|
|
// 如果直接返回 HTTP 响应对象,直接覆盖原响应对象
|
2025-01-06 08:45:23 +08:00
|
|
|
|
res = resOrObject as http.ServerResponse
|
|
|
|
|
} else {
|
2025-01-06 18:30:16 +08:00
|
|
|
|
// 处理自定义返回对象的情况
|
|
|
|
|
// 使用复杂的类型定义排除 ServerResponse 类型
|
2025-01-06 08:45:23 +08:00
|
|
|
|
type ExcludeServerResponse<T> = T extends http.ServerResponse ? never : T
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 将返回对象转换为非 ServerResponse 类型
|
2025-01-06 08:45:23 +08:00
|
|
|
|
const obj = resOrObject as ExcludeServerResponse<typeof resOrObject>
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 更新响应对象
|
2025-01-06 08:45:23 +08:00
|
|
|
|
res = obj.res
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 根据返回对象更新响应状态码
|
2025-01-06 08:45:23 +08:00
|
|
|
|
if (obj.status_code) responseData.status = obj.status_code
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 更新响应体
|
2025-01-06 08:45:23 +08:00
|
|
|
|
if (obj.body) responseData.body = obj.body
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 合并响应头,允许覆盖默认头
|
2025-01-06 08:45:23 +08:00
|
|
|
|
if (obj.headers)
|
|
|
|
|
responseData.headers = Object.assign(obj.headers, responseData.headers)
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
2025-01-06 18:30:16 +08:00
|
|
|
|
// 记录上传完成回调中的错误
|
2025-01-06 08:45:23 +08:00
|
|
|
|
log(`onUploadFinish: ${error.body}`)
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 抛出错误,中断上传流程
|
2025-01-06 08:45:23 +08:00
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 18:30:16 +08:00
|
|
|
|
|
2025-01-06 08:45:23 +08:00
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|