mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(settings): refresh upload limits and default to 4 GB
Share the saved upload limit with validation so changes apply without a page reload. Seed new installations with a 4096 MB limit while preserving existing values. Fixes #1733
This commit is contained in:
@@ -11,6 +11,7 @@ import MigrationModal from '@/features/migration/components/MigrationModal.vue'
|
||||
import { api } from '@/lib/api'
|
||||
import { getWorkflowState, type MigrationWorkflowState } from '@/features/migration/lib/migration-api'
|
||||
import { usePermissions } from '@/features/auth/composables/usePermissions'
|
||||
import { useAppInfo } from './composables/useAppInfo'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -29,7 +30,7 @@ const migrationLoading = ref(true)
|
||||
|
||||
const updateCheckEnabled = ref(true)
|
||||
const updateCheckLoading = ref(false)
|
||||
const maxUploadSizeMb = ref(500)
|
||||
const { maxUploadSizeMb } = useAppInfo()
|
||||
const localMaxUploadSizeMb = ref(500)
|
||||
const maxUploadSizeSaving = ref(false)
|
||||
const achievementBackfillRunning = ref(false)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { i18n } from '@/i18n'
|
||||
import { api } from '@/lib/api'
|
||||
import { useBookUpload } from '@/features/library/composables/useBookUpload'
|
||||
import { useAddBookFile } from '@/features/book/composables/useAddBookFile'
|
||||
import { useAppInfo } from '../composables/useAppInfo'
|
||||
import MaintenanceSettings from '../MaintenanceSettings.vue'
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(),
|
||||
getValidToken: vi.fn<() => Promise<string | null>>().mockResolvedValue(null),
|
||||
}))
|
||||
vi.mock('@/features/auth/composables/usePermissions', () => ({
|
||||
usePermissions: () => ({ isSuperuser: ref(true) }),
|
||||
}))
|
||||
vi.mock('@/features/migration/lib/migration-api', () => ({
|
||||
getWorkflowState: vi.fn<() => Promise<{ active: null }>>().mockResolvedValue({ active: null }),
|
||||
}))
|
||||
vi.mock('vue-sonner', () => ({ toast: { success: vi.fn<(message: string) => void>(), error: vi.fn<(message: string) => void>() } }))
|
||||
|
||||
const mockApi = vi.mocked(api)
|
||||
let wrapper: VueWrapper
|
||||
|
||||
function makeFile(sizeMb: number): File {
|
||||
const file = new File(['audio'], 'audiobook.m4b')
|
||||
Object.defineProperty(file, 'size', { value: sizeMb * 1024 * 1024 })
|
||||
return file
|
||||
}
|
||||
|
||||
async function mountSettings() {
|
||||
wrapper = mount(MaintenanceSettings, {
|
||||
global: { stubs: { MigrationModal: true, SettingsPageHeader: true, ToggleSwitch: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
async function saveLimit(sizeMb: number) {
|
||||
await wrapper.get('input[type="number"]').setValue(sizeMb)
|
||||
const save = wrapper.findAll('button').find((button) => button.text() === i18n.global.t('settings.admin.maintenance.save'))!
|
||||
await save.trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('MaintenanceSettings upload limit', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
i18n.global.locale.value = 'en'
|
||||
useAppInfo().maxUploadSizeMb.value = 500
|
||||
mockApi.mockResolvedValue(new Response(JSON.stringify([{ key: 'max_upload_size_mb', value: '500' }])))
|
||||
})
|
||||
|
||||
afterEach(() => wrapper?.unmount())
|
||||
|
||||
it.each([
|
||||
['library uploads', useBookUpload],
|
||||
['additional book files', useAddBookFile],
|
||||
] as const)('uses a newly saved higher limit for %s without a reload', async (_name, useUpload) => {
|
||||
const upload = useUpload()
|
||||
await mountSettings()
|
||||
mockApi.mockResolvedValueOnce(new Response(JSON.stringify({ key: 'max_upload_size_mb', value: '900' })))
|
||||
|
||||
await saveLimit(900)
|
||||
upload.addFiles([makeFile(600)])
|
||||
|
||||
expect(mockApi).toHaveBeenLastCalledWith('/api/v1/app-settings/max_upload_size_mb', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ value: '900' }),
|
||||
})
|
||||
expect(upload.files.value[0]?.status).toBe('pending')
|
||||
expect(upload.files.value[0]?.validationError).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['library uploads', useBookUpload],
|
||||
['additional book files', useAddBookFile],
|
||||
] as const)('uses a newly saved lower limit for %s without a reload', async (_name, useUpload) => {
|
||||
const upload = useUpload()
|
||||
await mountSettings()
|
||||
mockApi.mockResolvedValueOnce(new Response(JSON.stringify({ key: 'max_upload_size_mb', value: '200' })))
|
||||
|
||||
await saveLimit(200)
|
||||
upload.addFiles([makeFile(300)])
|
||||
|
||||
expect(upload.files.value[0]?.status).toBe('error')
|
||||
expect(upload.files.value[0]?.validationError).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the current limit when saving fails', async () => {
|
||||
await mountSettings()
|
||||
mockApi.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
||||
|
||||
await saveLimit(900)
|
||||
const upload = useBookUpload()
|
||||
upload.addFiles([makeFile(600)])
|
||||
|
||||
expect(useAppInfo().maxUploadSizeMb.value).toBe(500)
|
||||
expect(upload.files.value[0]?.status).toBe('error')
|
||||
})
|
||||
|
||||
it('uses the saved limit loaded by the settings page', async () => {
|
||||
mockApi.mockResolvedValueOnce(new Response(JSON.stringify([{ key: 'max_upload_size_mb', value: '900' }])))
|
||||
|
||||
await mountSettings()
|
||||
const upload = useBookUpload()
|
||||
upload.addFiles([makeFile(600)])
|
||||
|
||||
expect(upload.files.value[0]?.status).toBe('pending')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
export const DEFAULT_MAX_UPLOAD_SIZE_MB = 500;
|
||||
export const DEFAULT_MAX_UPLOAD_SIZE_MB = 4_096;
|
||||
export const HARD_MAX_UPLOAD_SIZE_MB = 4_096;
|
||||
export const HARD_MAX_UPLOAD_BYTES = HARD_MAX_UPLOAD_SIZE_MB * 1_024 * 1_024;
|
||||
export const UPLOAD_CHUNK_SIZE_BYTES = 16 * 1_024 * 1_024;
|
||||
|
||||
@@ -620,9 +620,9 @@ describe('AppSettingsService', () => {
|
||||
});
|
||||
|
||||
describe('getMaxUploadSizeMb', () => {
|
||||
it('returns 500 by default when setting is absent', async () => {
|
||||
it('returns 4096 by default when setting is absent', async () => {
|
||||
repo.findByKey.mockResolvedValue(undefined);
|
||||
expect(await service.getMaxUploadSizeMb()).toBe(500);
|
||||
expect(await service.getMaxUploadSizeMb()).toBe(4096);
|
||||
});
|
||||
|
||||
it('returns parsed integer value when valid', async () => {
|
||||
@@ -630,10 +630,15 @@ describe('AppSettingsService', () => {
|
||||
expect(await service.getMaxUploadSizeMb()).toBe(1024);
|
||||
});
|
||||
|
||||
it('returns 500 when value is invalid or <= 0', async () => {
|
||||
repo.findByKey.mockResolvedValue({ key: 'max_upload_size_mb', value: '-50' } as never);
|
||||
it('preserves an existing 500 MB limit', async () => {
|
||||
repo.findByKey.mockResolvedValue({ key: 'max_upload_size_mb', value: '500' } as never);
|
||||
expect(await service.getMaxUploadSizeMb()).toBe(500);
|
||||
});
|
||||
|
||||
it('returns 4096 when value is invalid or <= 0', async () => {
|
||||
repo.findByKey.mockResolvedValue({ key: 'max_upload_size_mb', value: '-50' } as never);
|
||||
expect(await service.getMaxUploadSizeMb()).toBe(4096);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update max_upload_size_mb validation', () => {
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('SeedService', () => {
|
||||
expect(insertedRows).toContainEqual({ key: APP_SETTING_KEYS.ALLOW_REGISTRATION, value: 'false' });
|
||||
expect(insertedRows).toContainEqual({ key: APP_SETTING_KEYS.OPDS_ENABLED, value: 'true' });
|
||||
expect(insertedRows).toContainEqual({ key: APP_SETTING_KEYS.CROSS_PLATFORM_PATH_SANITIZATION_ENABLED, value: 'true' });
|
||||
expect(insertedRows).toContainEqual({ key: APP_SETTING_KEYS.MAX_UPLOAD_SIZE_MB, value: '4096' });
|
||||
|
||||
const oidcRow = insertedRows.find((row) => row.key === APP_SETTING_KEYS.OIDC_CONFIG);
|
||||
expect(oidcRow).toBeDefined();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DEFAULT_AUTHOR_ENRICHMENT_CONFIG,
|
||||
DEFAULT_OIDC_CONFIG,
|
||||
} from '../../common/constants/app-settings.constants';
|
||||
import { DEFAULT_MAX_UPLOAD_SIZE_MB } from '../../common/constants/upload.constants';
|
||||
import { DB } from '../../db';
|
||||
import * as schema from '../../db/schema';
|
||||
import { getSanitizedErrorInfo } from './seed-log.util';
|
||||
@@ -41,7 +42,7 @@ const DEFAULT_APP_SETTINGS: Array<typeof schema.appSettings.$inferInsert> = [
|
||||
{ key: APP_SETTING_KEYS.OIDC_CONFIG, value: JSON.stringify(DEFAULT_OIDC_CONFIG) },
|
||||
{ key: APP_SETTING_KEYS.UPDATE_CHECK_ENABLED, value: 'true' },
|
||||
{ key: APP_SETTING_KEYS.CROSS_PLATFORM_PATH_SANITIZATION_ENABLED, value: 'true' },
|
||||
{ key: APP_SETTING_KEYS.MAX_UPLOAD_SIZE_MB, value: '500' },
|
||||
{ key: APP_SETTING_KEYS.MAX_UPLOAD_SIZE_MB, value: String(DEFAULT_MAX_UPLOAD_SIZE_MB) },
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
|
||||
Reference in New Issue
Block a user