Files
FileHost/server/api/videos/upload.post.ts
T
tianmo a64be50b73 feat: 完善媒体管理与备份迁移流程
- 视频上传生成缩略图并提供占位封面和失败原因展示
- 支持 HLS 按需重新封装 MP4,并限制临时文件生命周期与访问权限
- 完善视频、文件 API 文档、搜索、批量删除和单次转码质量参数
- 修复备份恢复与跨后端迁移遗漏缩略图、HLS 清单和分片的问题
- 将备份任务、归档和下载权限按用户隔离
- 增加仅清理当前用户数据的高危存储清空流程和二次文字确认
- 修复 S3 中文文件名元数据及 CLI 数据库连接问题
- 补充备份恢复、用户隔离、媒体资源和转码相关测试
2026-10-17 13:46:02 +08:00

133 lines
3.9 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 { readMultipartFormData } from 'h3'
import type { FileItem, FileUploadResponse } from '~/types/file'
import type { UploadErrorItem } from '~/types/image'
import {
getUploadUserId,
requireUploadAuth,
verifyApiUploadToken
} from '../../utils/access'
import { MAX_FILES_PER_UPLOAD } from '../../utils/constants'
import { createApiError } from '../../utils/api-error'
import { checkUploadRateLimit } from '../../utils/rate-limit'
import { processSingleVideoUpload } from '../../utils/file-bed'
import {
enqueueTranscode,
MAX_TRANSCODE_BITRATE_PERCENT,
MIN_TRANSCODE_BITRATE_PERCENT,
parseTranscodePreset
} from '../../utils/transcode'
import { getFileBedAccessMode } from '../../utils/env'
export default defineEventHandler(async (event) => {
await requireUploadAuth(event)
const formData = await readMultipartFormData(event)
if (!formData?.length) {
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
}
const tokenField = formData.find(part => part.name === 'token')
const formToken = tokenField?.data?.length
? new TextDecoder().decode(tokenField.data).trim()
: ''
checkUploadRateLimit(event, formToken)
const source = verifyApiUploadToken(event) ? 'api' : 'web'
const uploadUserId = await getUploadUserId(event, formToken)
const transcodeField = formData.find(part => part.name === 'transcode')
const wantTranscode = transcodeField?.data?.length
? ['1', 'true', 'yes'].includes(
new TextDecoder().decode(transcodeField.data).trim().toLowerCase()
)
: false
const presetField = formData.find(part => part.name === 'transcodePreset')
const presetRaw = presetField?.data?.length
? new TextDecoder().decode(presetField.data).trim()
: ''
const transcodePreset = presetRaw ? parseTranscodePreset(presetRaw) : undefined
if (presetRaw && !transcodePreset) {
createApiError(event, 'INVALID_REQUEST', '无效的转码质量档位', 400)
}
const bitrateField = formData.find(part => part.name === 'transcodeBitratePercent')
const bitrateRaw = bitrateField?.data?.length
? new TextDecoder().decode(bitrateField.data).trim()
: ''
const transcodeBitratePercent = bitrateRaw ? Number(bitrateRaw) : undefined
if (
bitrateRaw
&& (
!Number.isInteger(transcodeBitratePercent)
|| transcodeBitratePercent! < MIN_TRANSCODE_BITRATE_PERCENT
|| transcodeBitratePercent! > MAX_TRANSCODE_BITRATE_PERCENT
)
) {
createApiError(
event,
'INVALID_REQUEST',
`转码目标码率需为 ${MIN_TRANSCODE_BITRATE_PERCENT}–${MAX_TRANSCODE_BITRATE_PERCENT} 的整数`,
400
)
}
const fileParts = formData.filter(
part => (part.name === 'file' || part.name === 'files' || part.name === 'video')
&& part.data?.length
)
if (!fileParts.length) {
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
}
if (fileParts.length > MAX_FILES_PER_UPLOAD) {
createApiError(
event,
'TOO_MANY_FILES',
`每次最多上传 ${MAX_FILES_PER_UPLOAD} 个视频`,
400
)
}
const items: FileItem[] = []
const errors: UploadErrorItem[] = []
for (const part of fileParts) {
const result = await processSingleVideoUpload(event, {
bytes: new Uint8Array(part.data),
filename: part.filename,
contentType: part.type,
source,
userId: uploadUserId
})
if ('error' in result) {
errors.push(result.error)
} else {
items.push(result.item)
}
}
const response: FileUploadResponse = {
success: items.length > 0,
items,
errors,
accessMode: getFileBedAccessMode()
}
if (wantTranscode) {
for (const item of items) {
enqueueTranscode(item.key, {
...(transcodePreset ? { preset: transcodePreset } : {}),
...(transcodeBitratePercent !== undefined ? { bitratePercent: transcodeBitratePercent } : {})
})
}
}
if (!items.length && errors.length) {
setResponseStatus(event, 400)
}
return response
})