超大文件上传限制
This commit is contained in:
1 parent
885534378b
commit
3759422222
9 files changed
+249
-82
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { readMultipartFormData } from 'h3'
|
||||
import { readFormData } from 'h3'
|
||||
import type { FileItem, FileUploadResponse } from '~/types/file'
|
||||
import type { UploadErrorItem } from '~/types/image'
|
||||
import {
|
||||
@@ -11,7 +11,10 @@ import { createApiError } from '../../utils/api-error'
|
||||
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
||||
import { processSingleFileUpload } from '../../utils/file-bed'
|
||||
import { getFileBedAccessMode } from '../../utils/env'
|
||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
||||
import {
|
||||
readFormDataFieldValues,
|
||||
readFormDataFiles
|
||||
} from '../../utils/multipart-form'
|
||||
import {
|
||||
parseTagNamesParam,
|
||||
resolveUploadTagIds
|
||||
@@ -20,20 +23,18 @@ import {
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireUploadAuth(event)
|
||||
|
||||
const formData = await readMultipartFormData(event)
|
||||
if (!formData?.length) {
|
||||
const formData = await readFormData(event)
|
||||
if (!formData) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
}
|
||||
|
||||
const tokenField = formData.find(part => part.name === 'token')
|
||||
const formToken = tokenField?.data?.length
|
||||
? new TextDecoder().decode(tokenField.data).trim()
|
||||
: ''
|
||||
const tokenField = formData.get('token')
|
||||
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||
checkUploadRateLimit(event, formToken)
|
||||
|
||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||
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
|
||||
)
|
||||
@@ -43,8 +44,9 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
const uploadTagIds = resolvedTags.tagIds
|
||||
|
||||
const fileParts = formData.filter(
|
||||
part => (part.name === 'file' || part.name === 'files') && part.data?.length
|
||||
const fileParts = readFormDataFiles(
|
||||
formData,
|
||||
['file', 'files']
|
||||
)
|
||||
if (!fileParts.length) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
@@ -64,8 +66,8 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
for (const part of fileParts) {
|
||||
const result = await processSingleFileUpload(event, {
|
||||
bytes: new Uint8Array(part.data),
|
||||
filename: part.filename,
|
||||
bytes: new Uint8Array(await part.arrayBuffer()),
|
||||
filename: part.name,
|
||||
contentType: part.type,
|
||||
source,
|
||||
userId: uploadUserId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readMultipartFormData } from 'h3'
|
||||
import { readFormData } from 'h3'
|
||||
import type { UploadResponse, UploadErrorItem, ImageItem } from '~/types/image'
|
||||
import { requireUploadAuth, verifyApiUploadToken, getUploadUserId } from '../../utils/access'
|
||||
import { MAX_FILES_PER_UPLOAD } from '../../utils/constants'
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
} from '../../utils/process-image-upload'
|
||||
import { createApiError } from '../../utils/api-error'
|
||||
import { checkUploadRateLimit } from '../../utils/rate-limit'
|
||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
||||
import {
|
||||
readFormDataFieldValues,
|
||||
readFormDataFiles
|
||||
} from '../../utils/multipart-form'
|
||||
import {
|
||||
parseTagNamesParam,
|
||||
resolveUploadTagIds
|
||||
@@ -21,21 +24,19 @@ import {
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireUploadAuth(event)
|
||||
|
||||
const formData = await readMultipartFormData(event)
|
||||
if (!formData?.length) {
|
||||
const formData = await readFormData(event)
|
||||
if (!formData) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
}
|
||||
|
||||
const tokenField = formData.find(part => part.name === 'token')
|
||||
const formToken = tokenField?.data?.length
|
||||
? new TextDecoder().decode(tokenField.data).trim()
|
||||
: ''
|
||||
const tokenField = formData.get('token')
|
||||
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||
checkUploadRateLimit(event, formToken)
|
||||
|
||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||
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 resolvedTags = resolveUploadTagIds(uploadUserId, uploadTagNames)
|
||||
if ('message' in resolvedTags) {
|
||||
@@ -44,9 +45,9 @@ export default defineEventHandler(async (event) => {
|
||||
const uploadTagIds = resolvedTags.tagIds
|
||||
|
||||
// 兼容 image / file / files 字段名(图床脚本 / 通用客户端 / 网页多图)
|
||||
const fileParts = formData.filter(
|
||||
part => (part.name === 'image' || part.name === 'file' || part.name === 'files')
|
||||
&& part.data?.length
|
||||
const fileParts = readFormDataFiles(
|
||||
formData,
|
||||
['image', 'file', 'files']
|
||||
)
|
||||
if (!fileParts.length) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
@@ -65,8 +66,8 @@ export default defineEventHandler(async (event) => {
|
||||
const errors: UploadErrorItem[] = []
|
||||
|
||||
for (const part of fileParts) {
|
||||
const originalName = part.filename ?? 'image'
|
||||
const bytes = new Uint8Array(part.data)
|
||||
const originalName = part.name || 'image'
|
||||
const bytes = new Uint8Array(await part.arrayBuffer())
|
||||
const declaredMime = part.type?.toLowerCase() ?? ''
|
||||
const detectedMime = detectMimeFromSignature(bytes)
|
||||
|
||||
@@ -94,7 +95,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const result = await processSingleImageUpload(event, {
|
||||
bytes,
|
||||
filename: part.filename,
|
||||
filename: part.name,
|
||||
source,
|
||||
userId: uploadUserId,
|
||||
tagIds: uploadTagIds
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readMultipartFormData } from 'h3'
|
||||
import { readFormData } from 'h3'
|
||||
import type { FileItem, FileUploadResponse } from '~/types/file'
|
||||
import type { UploadErrorItem } from '~/types/image'
|
||||
import {
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
parseTranscodePreset
|
||||
} from '../../utils/transcode'
|
||||
import { getFileBedAccessMode } from '../../utils/env'
|
||||
import { readMultipartFormFieldValues } from '../../utils/multipart-form'
|
||||
import {
|
||||
readFormDataFieldValues,
|
||||
readFormDataFiles
|
||||
} from '../../utils/multipart-form'
|
||||
import {
|
||||
parseTagNamesParam,
|
||||
resolveUploadTagIds
|
||||
@@ -26,20 +29,18 @@ import {
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireUploadAuth(event)
|
||||
|
||||
const formData = await readMultipartFormData(event)
|
||||
if (!formData?.length) {
|
||||
const formData = await readFormData(event)
|
||||
if (!formData) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
}
|
||||
|
||||
const tokenField = formData.find(part => part.name === 'token')
|
||||
const formToken = tokenField?.data?.length
|
||||
? new TextDecoder().decode(tokenField.data).trim()
|
||||
: ''
|
||||
const tokenField = formData.get('token')
|
||||
const formToken = typeof tokenField === 'string' ? tokenField.trim() : ''
|
||||
checkUploadRateLimit(event, formToken)
|
||||
|
||||
const source = verifyApiUploadToken(event) ? 'api' : 'web'
|
||||
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
|
||||
)
|
||||
@@ -49,26 +50,19 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
const uploadTagIds = resolvedTags.tagIds
|
||||
|
||||
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 transcodeField = formData.get('transcode')
|
||||
const wantTranscode = typeof transcodeField === 'string'
|
||||
&& ['1', 'true', 'yes'].includes(transcodeField.trim().toLowerCase())
|
||||
|
||||
const presetField = formData.find(part => part.name === 'transcodePreset')
|
||||
const presetRaw = presetField?.data?.length
|
||||
? new TextDecoder().decode(presetField.data).trim()
|
||||
: ''
|
||||
const presetField = formData.get('transcodePreset')
|
||||
const presetRaw = typeof presetField === 'string' ? presetField.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 bitrateField = formData.get('transcodeBitratePercent')
|
||||
const bitrateRaw = typeof bitrateField === 'string' ? bitrateField.trim() : ''
|
||||
const transcodeBitratePercent = bitrateRaw ? Number(bitrateRaw) : undefined
|
||||
if (
|
||||
bitrateRaw
|
||||
@@ -86,9 +80,9 @@ export default defineEventHandler(async (event) => {
|
||||
)
|
||||
}
|
||||
|
||||
const fileParts = formData.filter(
|
||||
part => (part.name === 'file' || part.name === 'files' || part.name === 'video')
|
||||
&& part.data?.length
|
||||
const fileParts = readFormDataFiles(
|
||||
formData,
|
||||
['file', 'files', 'video']
|
||||
)
|
||||
if (!fileParts.length) {
|
||||
createApiError(event, 'INVALID_REQUEST', '未收到上传文件', 400)
|
||||
@@ -108,8 +102,8 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
for (const part of fileParts) {
|
||||
const result = await processSingleVideoUpload(event, {
|
||||
bytes: new Uint8Array(part.data),
|
||||
filename: part.filename,
|
||||
bytes: new Uint8Array(await part.arrayBuffer()),
|
||||
filename: part.name,
|
||||
contentType: part.type,
|
||||
source,
|
||||
userId: uploadUserId,
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
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(
|
||||
formData: MultiPartData[],
|
||||
fieldName: string
|
||||
): string[] {
|
||||
const field = formData.find(part => part.name === fieldName)
|
||||
if (field?.data?.length) {
|
||||
const raw = new TextDecoder().decode(field.data).trim()
|
||||
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)
|
||||
return parseFieldValues([new TextDecoder().decode(field.data)])
|
||||
}
|
||||
|
||||
const repeatFields = formData.filter(
|
||||
@@ -24,7 +30,25 @@ export function readMultipartFormFieldValues(
|
||||
)
|
||||
if (!repeatFields.length) return []
|
||||
|
||||
return repeatFields
|
||||
.map(part => new TextDecoder().decode(part.data!).trim())
|
||||
.filter(Boolean)
|
||||
return parseFieldValues(
|
||||
repeatFields.map(part => new TextDecoder().decode(part.data!))
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user