超大文件上传限制
This commit is contained in:
1 parent
885534378b
commit
3759422222
9 files changed
+249
-82
No files matched your search
@@ -5,6 +5,7 @@ import type {
|
|||||||
import { isUnauthorizedError } from './useAuth'
|
import { isUnauthorizedError } from './useAuth'
|
||||||
import { prepareFilesForUpload } from './useClipboardImage'
|
import { prepareFilesForUpload } from './useClipboardImage'
|
||||||
import { normalizeUploadTagNames } from './useUploadPreferences'
|
import { normalizeUploadTagNames } from './useUploadPreferences'
|
||||||
|
import { uploadFormDataWithProgress } from '~/utils/upload-progress'
|
||||||
|
|
||||||
const MAX_FILES = 10
|
const MAX_FILES = 10
|
||||||
|
|
||||||
@@ -66,10 +67,10 @@ export function useImageUpload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
progressItems.value = prepared.map((file, index) => ({
|
progressItems.value = prepared.map(file => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
progress: Math.round(((index + 1) / prepared.length) * 50)
|
progress: 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@@ -82,10 +83,16 @@ export function useImageUpload() {
|
|||||||
formData.append('tagNames', tagNames)
|
formData.append('tagNames', tagNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await $fetch<UploadResponse>('/api/images/upload', {
|
const response = await uploadFormDataWithProgress<UploadResponse>(
|
||||||
method: 'POST',
|
'/api/images/upload',
|
||||||
body: formData
|
formData,
|
||||||
})
|
(percent) => {
|
||||||
|
progressItems.value = progressItems.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
progress: Math.min(99, percent)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
lastUploadResult.value = response
|
lastUploadResult.value = response
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { FileItem, FileListResponse, FileUploadResponse } from '~/types/fil
|
|||||||
import type { CopyFormat, UploadMediaKind } from './useUploadPreferences'
|
import type { CopyFormat, UploadMediaKind } from './useUploadPreferences'
|
||||||
import { isUnauthorizedError } from './useAuth'
|
import { isUnauthorizedError } from './useAuth'
|
||||||
import { normalizeUploadTagNames } from './useUploadPreferences'
|
import { normalizeUploadTagNames } from './useUploadPreferences'
|
||||||
|
import { uploadFormDataWithProgress } from '~/utils/upload-progress'
|
||||||
|
|
||||||
const MAX_FILES = 10
|
const MAX_FILES = 10
|
||||||
|
|
||||||
@@ -255,13 +256,19 @@ export function useMediaUpload(kind: UploadMediaKind) {
|
|||||||
progressItems.value = progressItems.value.map(item => ({
|
progressItems.value = progressItems.value.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
progress: 50
|
progress: 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const response = await $fetch<FileUploadResponse>(endpoint, {
|
const response = await uploadFormDataWithProgress<FileUploadResponse>(
|
||||||
method: 'POST',
|
endpoint,
|
||||||
body: formData
|
formData,
|
||||||
})
|
(percent) => {
|
||||||
|
progressItems.value = progressItems.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
progress: Math.min(99, percent)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
lastResult.value = response
|
lastResult.value = response
|
||||||
accessMode.value = response.accessMode
|
accessMode.value = response.accessMode
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { uploadFormDataWithProgress } from './upload-progress'
|
||||||
|
|
||||||
|
class FakeXhr {
|
||||||
|
static current: FakeXhr
|
||||||
|
|
||||||
|
upload = {
|
||||||
|
onprogress: null as ((event: ProgressEvent) => void) | null
|
||||||
|
}
|
||||||
|
|
||||||
|
status = 200
|
||||||
|
statusText = 'OK'
|
||||||
|
responseText = '{"ok":true}'
|
||||||
|
withCredentials = false
|
||||||
|
onload: (() => void) | null = null
|
||||||
|
onerror: (() => void) | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
FakeXhr.current = this
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {}
|
||||||
|
|
||||||
|
send() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('uploadFormDataWithProgress', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports the uploaded byte percentage', async () => {
|
||||||
|
vi.stubGlobal('XMLHttpRequest', FakeXhr)
|
||||||
|
const onProgress = vi.fn()
|
||||||
|
const result = uploadFormDataWithProgress<{ ok: boolean }>(
|
||||||
|
'/upload',
|
||||||
|
new FormData(),
|
||||||
|
onProgress
|
||||||
|
)
|
||||||
|
|
||||||
|
FakeXhr.current.upload.onprogress!({
|
||||||
|
lengthComputable: true,
|
||||||
|
loaded: 40,
|
||||||
|
total: 100
|
||||||
|
} as ProgressEvent)
|
||||||
|
FakeXhr.current.onload!()
|
||||||
|
|
||||||
|
await expect(result).resolves.toEqual({ ok: true })
|
||||||
|
expect(onProgress).toHaveBeenCalledWith(40)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the HTTP status and server error message', async () => {
|
||||||
|
vi.stubGlobal('XMLHttpRequest', FakeXhr)
|
||||||
|
const result = uploadFormDataWithProgress(
|
||||||
|
'/upload',
|
||||||
|
new FormData(),
|
||||||
|
() => {}
|
||||||
|
)
|
||||||
|
|
||||||
|
FakeXhr.current.status = 401
|
||||||
|
FakeXhr.current.statusText = 'Unauthorized'
|
||||||
|
FakeXhr.current.responseText = JSON.stringify({
|
||||||
|
data: { error: { message: '未登录' } }
|
||||||
|
})
|
||||||
|
FakeXhr.current.onload!()
|
||||||
|
|
||||||
|
await expect(result).rejects.toMatchObject({
|
||||||
|
statusCode: 401,
|
||||||
|
message: '未登录'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
interface UploadError extends Error {
|
||||||
|
statusCode: number
|
||||||
|
statusMessage: string
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadErrorMessage(data: unknown, fallback: string): string {
|
||||||
|
if (!data || typeof data !== 'object') return fallback
|
||||||
|
|
||||||
|
const body = data as {
|
||||||
|
message?: unknown
|
||||||
|
error?: { message?: unknown }
|
||||||
|
data?: { error?: { message?: unknown } }
|
||||||
|
}
|
||||||
|
const message = body.data?.error?.message ?? body.error?.message ?? body.message
|
||||||
|
return typeof message === 'string' ? message : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadFormDataWithProgress<T>(
|
||||||
|
url: string,
|
||||||
|
formData: FormData,
|
||||||
|
onProgress: (percent: number) => void
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('POST', url)
|
||||||
|
xhr.withCredentials = true
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable && event.total > 0) {
|
||||||
|
onProgress(Math.round((event.loaded / event.total) * 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
let data: unknown = null
|
||||||
|
try {
|
||||||
|
data = xhr.responseText ? JSON.parse(xhr.responseText) : null
|
||||||
|
} catch {
|
||||||
|
// 非 JSON 响应交给状态码和状态文本报错
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(data as T)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new Error(
|
||||||
|
uploadErrorMessage(data, xhr.statusText || 'Upload failed')
|
||||||
|
) as UploadError
|
||||||
|
error.statusCode = xhr.status
|
||||||
|
error.statusMessage = xhr.statusText
|
||||||
|
error.data = data
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new Error('Network error'))
|
||||||
|
xhr.send(formData)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readMultipartFormData } from 'h3'
|
import { readFormData } from 'h3'
|
||||||
import type { FileItem, FileUploadResponse } from '~/types/file'
|
import type { FileItem, FileUploadResponse } from '~/types/file'
|
||||||
import type { UploadErrorItem } from '~/types/image'
|
import type { UploadErrorItem } from '~/types/image'
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,10 @@ import { createApiError } from '../../utils/api-error'
|
|||||||
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
||||||
import { processSingleFileUpload } from '../../utils/file-bed'
|
import { processSingleFileUpload } from '../../utils/file-bed'
|
||||||
import { getFileBedAccessMode } from '../../utils/env'
|
import { getFileBedAccessMode } from '../../utils/env'
|
||||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
import {
|
||||||
|
readFormDataFieldValues,
|
||||||
|
readFormDataFiles
|
||||||
|
} from '../../utils/multipart-form'
|
||||||
import {
|
import {
|
||||||
parseTagNamesParam,
|
parseTagNamesParam,
|
||||||
resolveUploadTagIds
|
resolveUploadTagIds
|
||||||
@@ -20,20 +23,18 @@ import {
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await requireUploadAuth(event)
|
await requireUploadAuth(event)
|
||||||
|
|
||||||
const formData = await readMultipartFormData(event)
|
const formData = await readFormData(event)
|
||||||
if (!formData?.length) {
|
if (!formData) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenField = formData.find(part => part.name === 'token')
|
const tokenField = formData.get('token')
|
||||||
const formToken = tokenField?.data?.length
|
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||||
? new TextDecoder().decode(tokenField.data).trim()
|
|
||||||
: ''
|
|
||||||
checkUploadRateLimit(event, formToken)
|
checkUploadRateLimit(event, formToken)
|
||||||
|
|
||||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||||
const uploadUserId = await getUploadUserId(event, formToken)
|
const uploadUserId = await getUploadUserId(event, formToken)
|
||||||
const tagNamesRaw = readMultipartFormFieldValues(formData, 'tagNames')
|
const tagNamesRaw = readFormDataFieldValues(formData, 'tagNames')
|
||||||
const uploadTagNames = parseTagNamesParam(
|
const uploadTagNames = parseTagNamesParam(
|
||||||
tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw
|
tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw
|
||||||
)
|
)
|
||||||
@@ -43,8 +44,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
const uploadTagIds = resolvedTags.tagIds
|
const uploadTagIds = resolvedTags.tagIds
|
||||||
|
|
||||||
const fileParts = formData.filter(
|
const fileParts = readFormDataFiles(
|
||||||
part => (part.name === 'file' || part.name === 'files') && part.data?.length
|
formData,
|
||||||
|
['file', 'files']
|
||||||
)
|
)
|
||||||
if (!fileParts.length) {
|
if (!fileParts.length) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
@@ -64,8 +66,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
for (const part of fileParts) {
|
for (const part of fileParts) {
|
||||||
const result = await processSingleFileUpload(event, {
|
const result = await processSingleFileUpload(event, {
|
||||||
bytes: new Uint8Array(part.data),
|
bytes: new Uint8Array(await part.arrayBuffer()),
|
||||||
filename: part.filename,
|
filename: part.name,
|
||||||
contentType: part.type,
|
contentType: part.type,
|
||||||
source,
|
source,
|
||||||
userId: uploadUserId,
|
userId: uploadUserId,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readMultipartFormData } from 'h3'
|
import { readFormData } from 'h3'
|
||||||
import type { UploadResponse, UploadErrorItem, ImageItem } from '~/types/image'
|
import type { UploadResponse, UploadErrorItem, ImageItem } from '~/types/image'
|
||||||
import { requireUploadAuth, verifyApiUploadToken, getUploadUserId } from '../../utils/access'
|
import { requireUploadAuth, verifyApiUploadToken, getUploadUserId } from '../../utils/access'
|
||||||
import { MAX_FILES_PER_UPLOAD } from '../../utils/constants'
|
import { MAX_FILES_PER_UPLOAD } from '../../utils/constants'
|
||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
} from '../../utils/process-image-upload'
|
} from '../../utils/process-image-upload'
|
||||||
import { createApiError } from '../../utils/api-error'
|
import { createApiError } from '../../utils/api-error'
|
||||||
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
||||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
import {
|
||||||
|
readFormDataFieldValues,
|
||||||
|
readFormDataFiles
|
||||||
|
} from '../../utils/multipart-form'
|
||||||
import {
|
import {
|
||||||
parseTagNamesParam,
|
parseTagNamesParam,
|
||||||
resolveUploadTagIds
|
resolveUploadTagIds
|
||||||
@@ -21,21 +24,19 @@ import {
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await requireUploadAuth(event)
|
await requireUploadAuth(event)
|
||||||
|
|
||||||
const formData = await readMultipartFormData(event)
|
const formData = await readFormData(event)
|
||||||
if (!formData?.length) {
|
if (!formData) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenField = formData.find(part => part.name === 'token')
|
const tokenField = formData.get('token')
|
||||||
const formToken = tokenField?.data?.length
|
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||||
? new TextDecoder().decode(tokenField.data).trim()
|
|
||||||
: ''
|
|
||||||
checkUploadRateLimit(event, formToken)
|
checkUploadRateLimit(event, formToken)
|
||||||
|
|
||||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||||
const uploadUserId = await getUploadUserId(event, formToken)
|
const uploadUserId = await getUploadUserId(event, formToken)
|
||||||
|
|
||||||
const tagNamesRaw = readMultipartFormFieldValues(formData, 'tagNames')
|
const tagNamesRaw = readFormDataFieldValues(formData, 'tagNames')
|
||||||
const uploadTagNames = parseTagNamesParam(tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw)
|
const uploadTagNames = parseTagNamesParam(tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw)
|
||||||
const resolvedTags = resolveUploadTagIds(uploadUserId, uploadTagNames)
|
const resolvedTags = resolveUploadTagIds(uploadUserId, uploadTagNames)
|
||||||
if ('message' in resolvedTags) {
|
if ('message' in resolvedTags) {
|
||||||
@@ -44,9 +45,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
const uploadTagIds = resolvedTags.tagIds
|
const uploadTagIds = resolvedTags.tagIds
|
||||||
|
|
||||||
// 兼容 image / file / files 字段名(图床脚本 / 通用客户端 / 网页多图)
|
// 兼容 image / file / files 字段名(图床脚本 / 通用客户端 / 网页多图)
|
||||||
const fileParts = formData.filter(
|
const fileParts = readFormDataFiles(
|
||||||
part => (part.name === 'image' || part.name === 'file' || part.name === 'files')
|
formData,
|
||||||
&& part.data?.length
|
['image', 'file', 'files']
|
||||||
)
|
)
|
||||||
if (!fileParts.length) {
|
if (!fileParts.length) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
@@ -65,8 +66,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
const errors: UploadErrorItem[] = []
|
const errors: UploadErrorItem[] = []
|
||||||
|
|
||||||
for (const part of fileParts) {
|
for (const part of fileParts) {
|
||||||
const originalName = part.filename ?? 'image'
|
const originalName = part.name || 'image'
|
||||||
const bytes = new Uint8Array(part.data)
|
const bytes = new Uint8Array(await part.arrayBuffer())
|
||||||
const declaredMime = part.type?.toLowerCase() ?? ''
|
const declaredMime = part.type?.toLowerCase() ?? ''
|
||||||
const detectedMime = detectMimeFromSignature(bytes)
|
const detectedMime = detectMimeFromSignature(bytes)
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const result = await processSingleImageUpload(event, {
|
const result = await processSingleImageUpload(event, {
|
||||||
bytes,
|
bytes,
|
||||||
filename: part.filename,
|
filename: part.name,
|
||||||
source,
|
source,
|
||||||
userId: uploadUserId,
|
userId: uploadUserId,
|
||||||
tagIds: uploadTagIds
|
tagIds: uploadTagIds
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readMultipartFormData } from 'h3'
|
import { readFormData } from 'h3'
|
||||||
import type { FileItem, FileUploadResponse } from '~/types/file'
|
import type { FileItem, FileUploadResponse } from '~/types/file'
|
||||||
import type { UploadErrorItem } from '~/types/image'
|
import type { UploadErrorItem } from '~/types/image'
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
parseTranscodePreset
|
parseTranscodePreset
|
||||||
} from '../../utils/transcode'
|
} from '../../utils/transcode'
|
||||||
import { getFileBedAccessMode } from '../../utils/env'
|
import { getFileBedAccessMode } from '../../utils/env'
|
||||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
import {
|
||||||
|
readFormDataFieldValues,
|
||||||
|
readFormDataFiles
|
||||||
|
} from '../../utils/multipart-form'
|
||||||
import {
|
import {
|
||||||
parseTagNamesParam,
|
parseTagNamesParam,
|
||||||
resolveUploadTagIds
|
resolveUploadTagIds
|
||||||
@@ -26,20 +29,18 @@ import {
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await requireUploadAuth(event)
|
await requireUploadAuth(event)
|
||||||
|
|
||||||
const formData = await readMultipartFormData(event)
|
const formData = await readFormData(event)
|
||||||
if (!formData?.length) {
|
if (!formData) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenField = formData.find(part => part.name === 'token')
|
const tokenField = formData.get('token')
|
||||||
const formToken = tokenField?.data?.length
|
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||||
? new TextDecoder().decode(tokenField.data).trim()
|
|
||||||
: ''
|
|
||||||
checkUploadRateLimit(event, formToken)
|
checkUploadRateLimit(event, formToken)
|
||||||
|
|
||||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||||
const uploadUserId = await getUploadUserId(event, formToken)
|
const uploadUserId = await getUploadUserId(event, formToken)
|
||||||
const tagNamesRaw = readMultipartFormFieldValues(formData, 'tagNames')
|
const tagNamesRaw = readFormDataFieldValues(formData, 'tagNames')
|
||||||
const uploadTagNames = parseTagNamesParam(
|
const uploadTagNames = parseTagNamesParam(
|
||||||
tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw
|
tagNamesRaw.length === 1 ? tagNamesRaw[0] : tagNamesRaw
|
||||||
)
|
)
|
||||||
@@ -49,26 +50,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
const uploadTagIds = resolvedTags.tagIds
|
const uploadTagIds = resolvedTags.tagIds
|
||||||
|
|
||||||
const transcodeField = formData.find(part => part.name === 'transcode')
|
const transcodeField = formData.get('transcode')
|
||||||
const wantTranscode = transcodeField?.data?.length
|
const wantTranscode = typeof transcodeField === 'string'
|
||||||
? ['1', 'true', 'yes'].includes(
|
&& ['1', 'true', 'yes'].includes(transcodeField.trim().toLowerCase())
|
||||||
new TextDecoder().decode(transcodeField.data).trim().toLowerCase()
|
|
||||||
)
|
|
||||||
: false
|
|
||||||
|
|
||||||
const presetField = formData.find(part => part.name === 'transcodePreset')
|
const presetField = formData.get('transcodePreset')
|
||||||
const presetRaw = presetField?.data?.length
|
const presetRaw = typeof presetField === 'string' ? presetField.trim() : ''
|
||||||
? new TextDecoder().decode(presetField.data).trim()
|
|
||||||
: ''
|
|
||||||
const transcodePreset = presetRaw ? parseTranscodePreset(presetRaw) : undefined
|
const transcodePreset = presetRaw ? parseTranscodePreset(presetRaw) : undefined
|
||||||
if (presetRaw && !transcodePreset) {
|
if (presetRaw && !transcodePreset) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '无效的转码质量档位', 400)
|
createApiError(event, 'INVALID_REQUEST', '无效的转码质量档位', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitrateField = formData.find(part => part.name === 'transcodeBitratePercent')
|
const bitrateField = formData.get('transcodeBitratePercent')
|
||||||
const bitrateRaw = bitrateField?.data?.length
|
const bitrateRaw = typeof bitrateField === 'string' ? bitrateField.trim() : ''
|
||||||
? new TextDecoder().decode(bitrateField.data).trim()
|
|
||||||
: ''
|
|
||||||
const transcodeBitratePercent = bitrateRaw ? Number(bitrateRaw) : undefined
|
const transcodeBitratePercent = bitrateRaw ? Number(bitrateRaw) : undefined
|
||||||
if (
|
if (
|
||||||
bitrateRaw
|
bitrateRaw
|
||||||
@@ -86,9 +80,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileParts = formData.filter(
|
const fileParts = readFormDataFiles(
|
||||||
part => (part.name === 'file' || part.name === 'files' || part.name === 'video')
|
formData,
|
||||||
&& part.data?.length
|
['file', 'files', 'video']
|
||||||
)
|
)
|
||||||
if (!fileParts.length) {
|
if (!fileParts.length) {
|
||||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||||
@@ -108,8 +102,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
for (const part of fileParts) {
|
for (const part of fileParts) {
|
||||||
const result = await processSingleVideoUpload(event, {
|
const result = await processSingleVideoUpload(event, {
|
||||||
bytes: new Uint8Array(part.data),
|
bytes: new Uint8Array(await part.arrayBuffer()),
|
||||||
filename: part.filename,
|
filename: part.name,
|
||||||
contentType: part.type,
|
contentType: part.type,
|
||||||
source,
|
source,
|
||||||
userId: uploadUserId,
|
userId: uploadUserId,
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import type { MultiPartData } from 'h3'
|
import type { MultiPartData } from 'h3'
|
||||||
|
|
||||||
|
function parseFieldValues(rawValues: string[]): string[] {
|
||||||
|
const values = rawValues.map(value => value.trim()).filter(Boolean)
|
||||||
|
if (values.length !== 1) return values
|
||||||
|
|
||||||
|
const raw = values[0]!
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map(item => String(item).trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to comma-separated parsing
|
||||||
|
}
|
||||||
|
return raw.split(',').map(item => item.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
export function readMultipartFormFieldValues(
|
export function readMultipartFormFieldValues(
|
||||||
formData: MultiPartData[],
|
formData: MultiPartData[],
|
||||||
fieldName: string
|
fieldName: string
|
||||||
): string[] {
|
): string[] {
|
||||||
const field = formData.find(part => part.name === fieldName)
|
const field = formData.find(part => part.name === fieldName)
|
||||||
if (field?.data?.length) {
|
if (field?.data?.length) {
|
||||||
const raw = new TextDecoder().decode(field.data).trim()
|
return parseFieldValues([new TextDecoder().decode(field.data)])
|
||||||
if (!raw) return []
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw) as unknown
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return parsed.map(item => String(item).trim()).filter(Boolean)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through to comma-separated parsing
|
|
||||||
}
|
|
||||||
return raw.split(',').map(item => item.trim()).filter(Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const repeatFields = formData.filter(
|
const repeatFields = formData.filter(
|
||||||
@@ -24,7 +30,25 @@ export function readMultipartFormFieldValues(
|
|||||||
)
|
)
|
||||||
if (!repeatFields.length) return []
|
if (!repeatFields.length) return []
|
||||||
|
|
||||||
return repeatFields
|
return parseFieldValues(
|
||||||
.map(part => new TextDecoder().decode(part.data!).trim())
|
repeatFields.map(part => new TextDecoder().decode(part.data!))
|
||||||
.filter(Boolean)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readFormDataFieldValues(
|
||||||
|
formData: FormData,
|
||||||
|
fieldName: string
|
||||||
|
): string[] {
|
||||||
|
return parseFieldValues(
|
||||||
|
formData.getAll(fieldName).filter((value): value is string => typeof value === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readFormDataFiles(
|
||||||
|
formData: FormData,
|
||||||
|
fieldNames: string[]
|
||||||
|
): File[] {
|
||||||
|
return fieldNames
|
||||||
|
.flatMap(name => formData.getAll(name))
|
||||||
|
.filter((value): value is File => value instanceof File && value.size > 0)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
include: ['server/**/*.test.ts'],
|
include: ['server/**/*.test.ts', 'app/utils/**/*.test.ts'],
|
||||||
pool: 'forks',
|
pool: 'forks',
|
||||||
setupFiles: ['./server/test/setup.ts']
|
setupFiles: ['./server/test/setup.ts']
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user