fix(frontend): relax system import limits

This commit is contained in:
fawney19
2026-05-16 19:00:20 +08:00
parent 328ac721ce
commit 56994d4c29
4 changed files with 182 additions and 9 deletions

View File

@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AggregateImportRequest, ConfigImportRequest, UsersImportRequest } from '@/api/admin'
const { postMock } = vi.hoisted(() => ({
postMock: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: {
post: postMock,
},
}))
import { adminApi } from '@/api/admin'
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
describe('adminApi system data import timeouts', () => {
beforeEach(() => {
postMock.mockReset()
postMock.mockResolvedValue({ data: {} })
})
it('uses a long timeout for config imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
merge_mode: 'skip',
} satisfies ConfigImportRequest
await adminApi.importConfig(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/config/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
it('uses a long timeout for user imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
users: [],
merge_mode: 'skip',
} satisfies UsersImportRequest
await adminApi.importUsers(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/users/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
it('uses a long timeout for aggregate imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
config_data: {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
},
user_data: {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
users: [],
},
merge_mode: 'skip',
} satisfies AggregateImportRequest
await adminApi.importAggregateData(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/data/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
})

View File

@@ -4,6 +4,8 @@ import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
function extractConflictPayload(error: unknown): ManualUsageCleanupConflict | null {
if (!axios.isAxiosError(error) || error.response?.status !== 409) {
return null
@@ -854,7 +856,8 @@ export const adminApi = {
async importConfig(data: ConfigImportRequest): Promise<ConfigImportResponse> {
const response = await apiClient.post<ConfigImportResponse>(
'/api/admin/system/config/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},
@@ -869,7 +872,8 @@ export const adminApi = {
async importUsers(data: UsersImportRequest): Promise<UsersImportResponse> {
const response = await apiClient.post<UsersImportResponse>(
'/api/admin/system/users/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},
@@ -884,7 +888,8 @@ export const adminApi = {
async importAggregateData(data: AggregateImportRequest): Promise<AggregateImportResponse> {
const response = await apiClient.post<AggregateImportResponse>(
'/api/admin/system/data/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},

View File

@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
const { errorMock, successMock } = vi.hoisted(() => ({
errorMock: vi.fn(),
successMock: vi.fn(),
}))
vi.mock('@/composables/useToast', () => ({
useToast: () => ({
error: errorMock,
success: successMock,
}),
}))
vi.mock('@/api/admin', () => ({
adminApi: {},
}))
import { useConfigExportImport } from '../composables/useConfigExportImport'
function buildFileInputEvent(file: File): Event {
return {
target: {
files: [file],
value: 'selected.json',
},
} as unknown as Event
}
function makeSizedFile(content: string, size: number): File {
const file = new File([content], 'config.json', { type: 'application/json' })
Object.defineProperty(file, 'size', { value: size })
return file
}
describe('useConfigExportImport file size limits', () => {
beforeEach(() => {
errorMock.mockReset()
successMock.mockReset()
})
it('accepts config import files larger than the old 10MB limit', async () => {
const state = useConfigExportImport(ref({ site_name: 'Aether' }))
const file = makeSizedFile(
JSON.stringify({
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
}),
11 * 1024 * 1024
)
state.handleConfigFileSelect(buildFileInputEvent(file))
await vi.waitFor(() => expect(state.importDialogOpen.value).toBe(true))
expect(errorMock).not.toHaveBeenCalledWith('文件大小不能超过 10MB')
expect(state.importPreview.value).toEqual({
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
})
expect(state.importDialogOpen.value).toBe(true)
})
it('shows the updated config import limit when the file is too large', () => {
const state = useConfigExportImport(ref({ site_name: 'Aether' }))
const file = makeSizedFile('{}', 501 * 1024 * 1024)
state.handleConfigFileSelect(buildFileInputEvent(file))
expect(errorMock).toHaveBeenCalledWith('文件大小不能超过 500MB')
})
})

View File

@@ -13,9 +13,12 @@ import { parseApiError } from '@/utils/errorParser'
import { log } from '@/utils/logger'
import type { SystemConfig } from './useSystemConfig'
// 文件大小限制:聚合数据包含配置和用户数据,允许更大的备份文件
const MAX_FILE_SIZE = 10 * 1024 * 1024
const MAX_AGGREGATE_FILE_SIZE = 20 * 1024 * 1024
// 文件大小限制:导出文件可能包含大量 Provider Key、模型和用户数据
const BYTES_PER_MB = 1024 * 1024
const MAX_FILE_SIZE_MB = 500
const MAX_AGGREGATE_FILE_SIZE_MB = 500
const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * BYTES_PER_MB
const MAX_AGGREGATE_FILE_SIZE = MAX_AGGREGATE_FILE_SIZE_MB * BYTES_PER_MB
type JsonObject = Record<string, unknown>
@@ -49,6 +52,10 @@ function looksLikeAggregateExport(value: JsonObject): boolean {
&& asJsonObject(value.user_data) != null
}
function fileSizeLimitMessage(limitMb: number): string {
return `文件大小不能超过 ${limitMb}MB`
}
function downloadJson(data: unknown, filename: string) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
@@ -126,7 +133,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
if (!file) return
if (file.size > MAX_FILE_SIZE) {
error('文件大小不能超过 10MB')
error(fileSizeLimitMessage(MAX_FILE_SIZE_MB))
input.value = ''
return
}
@@ -223,7 +230,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
if (!file) return
if (file.size > MAX_FILE_SIZE) {
error('文件大小不能超过 10MB')
error(fileSizeLimitMessage(MAX_FILE_SIZE_MB))
input.value = ''
return
}
@@ -325,7 +332,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
if (!file) return
if (file.size > MAX_AGGREGATE_FILE_SIZE) {
error('文件大小不能超过 20MB')
error(fileSizeLimitMessage(MAX_AGGREGATE_FILE_SIZE_MB))
input.value = ''
return
}