超大文件上传限制

This commit is contained in:
tianmo committed 2026-10-19 15:04:41 +08:00
1 parent 885534378b
commit 3759422222
9 files changed
+249 -82

No files matched your search

+13 -6
View File
@@ -5,6 +5,7 @@ import type {
import { isUnauthorizedError } from './useAuth'
import { prepareFilesForUpload } from './useClipboardImage'
import { normalizeUploadTagNames } from './useUploadPreferences'
import { uploadFormDataWithProgress } from '~/utils/upload-progress'
const MAX_FILES = 10
@@ -66,10 +67,10 @@ export function useImageUpload() {
return
}
progressItems.value = prepared.map((file, index) => ({
progressItems.value = prepared.map(file => ({
name: file.name,
status: 'uploading',
progress: Math.round(((index + 1) / prepared.length) * 50)
progress: 0
}))
const formData = new FormData()
@@ -82,10 +83,16 @@ export function useImageUpload() {
formData.append('tagNames', tagNames)
}
const response = await $fetch<UploadResponse>('/api/images/upload', {
method: 'POST',
body: formData
})
const response = await uploadFormDataWithProgress<UploadResponse>(
'/api/images/upload',
formData,
(percent) => {
progressItems.value = progressItems.value.map(item => ({
...item,
progress: Math.min(99, percent)
}))
}
)
lastUploadResult.value = response
+12 -5
View File
@@ -3,6 +3,7 @@ import type { FileItem, FileListResponse, FileUploadResponse } from '~/types/fil
import type { CopyFormat, UploadMediaKind } from './useUploadPreferences'
import { isUnauthorizedError } from './useAuth'
import { normalizeUploadTagNames } from './useUploadPreferences'
import { uploadFormDataWithProgress } from '~/utils/upload-progress'
const MAX_FILES = 10
@@ -255,13 +256,19 @@ export function useMediaUpload(kind: UploadMediaKind) {
progressItems.value = progressItems.value.map(item => ({
...item,
status: 'uploading',
progress: 50
progress: 0
}))
const response = await $fetch<FileUploadResponse>(endpoint, {
method: 'POST',
body: formData
})
const response = await uploadFormDataWithProgress<FileUploadResponse>(
endpoint,
formData,
(percent) => {
progressItems.value = progressItems.value.map(item => ({
...item,
progress: Math.min(99, percent)
}))
}
)
lastResult.value = response
accessMode.value = response.accessMode
+72
View File
@@ -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: '未登录'
})
})
})
+60
View File
@@ -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)
})
}