Merge pull request #212 from the-luap/revert-210-feat/configurable-upload-batch-size
Revert "feat: configurable upload batch size for reverse proxy compatibility"
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
exports.up = async function(knex) {
|
|
||||||
const exists = await knex('app_settings')
|
|
||||||
.where({ setting_key: 'general_max_upload_batch_size_mb' })
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
await knex('app_settings').insert({
|
|
||||||
setting_key: 'general_max_upload_batch_size_mb',
|
|
||||||
setting_value: JSON.stringify(95),
|
|
||||||
setting_type: 'general',
|
|
||||||
updated_at: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = async function(knex) {
|
|
||||||
await knex('app_settings')
|
|
||||||
.where({ setting_key: 'general_max_upload_batch_size_mb' })
|
|
||||||
.del();
|
|
||||||
};
|
|
||||||
@@ -98,8 +98,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
|||||||
|
|
||||||
// For large uploads, chunk the files by both count AND size to prevent memory/network issues
|
// For large uploads, chunk the files by both count AND size to prevent memory/network issues
|
||||||
const MAX_FILES_PER_CHUNK = Math.max(1, Math.min(50, maxFilesPerUpload)); // Max 50 files per chunk
|
const MAX_FILES_PER_CHUNK = Math.max(1, Math.min(50, maxFilesPerUpload)); // Max 50 files per chunk
|
||||||
const maxBatchSizeMb = Number(settings?.general_max_upload_batch_size_mb) || 95;
|
const MAX_BYTES_PER_CHUNK = 500 * 1024 * 1024; // Max 500MB per chunk (nginx limit is 1GB)
|
||||||
const MAX_BYTES_PER_CHUNK = maxBatchSizeMb * 1024 * 1024;
|
|
||||||
const chunks: File[][] = [];
|
const chunks: File[][] = [];
|
||||||
|
|
||||||
let currentChunk: File[] = [];
|
let currentChunk: File[] = [];
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface GeneralSettings {
|
|||||||
max_file_size_mb: number;
|
max_file_size_mb: number;
|
||||||
max_files_per_upload: number;
|
max_files_per_upload: number;
|
||||||
allowed_file_types: string;
|
allowed_file_types: string;
|
||||||
max_upload_batch_size_mb: number;
|
|
||||||
enable_analytics: boolean;
|
enable_analytics: boolean;
|
||||||
enable_registration: boolean;
|
enable_registration: boolean;
|
||||||
maintenance_mode: boolean;
|
maintenance_mode: boolean;
|
||||||
@@ -88,7 +87,6 @@ export function useSettingsState() {
|
|||||||
max_file_size_mb: 50,
|
max_file_size_mb: 50,
|
||||||
max_files_per_upload: 500,
|
max_files_per_upload: 500,
|
||||||
allowed_file_types: 'jpg,jpeg,png,gif,webp',
|
allowed_file_types: 'jpg,jpeg,png,gif,webp',
|
||||||
max_upload_batch_size_mb: 95,
|
|
||||||
enable_analytics: true,
|
enable_analytics: true,
|
||||||
enable_registration: false,
|
enable_registration: false,
|
||||||
maintenance_mode: false,
|
maintenance_mode: false,
|
||||||
@@ -171,7 +169,6 @@ export function useSettingsState() {
|
|||||||
Math.max(1, toNumber(settings.general_max_files_per_upload, 500))
|
Math.max(1, toNumber(settings.general_max_files_per_upload, 500))
|
||||||
),
|
),
|
||||||
allowed_file_types: settings.general_allowed_file_types || 'jpg,jpeg,png,gif,webp',
|
allowed_file_types: settings.general_allowed_file_types || 'jpg,jpeg,png,gif,webp',
|
||||||
max_upload_batch_size_mb: toNumber(settings.general_max_upload_batch_size_mb, 95),
|
|
||||||
enable_analytics: toBoolean(settings.general_enable_analytics, true),
|
enable_analytics: toBoolean(settings.general_enable_analytics, true),
|
||||||
enable_registration: toBoolean(settings.general_enable_registration, false),
|
enable_registration: toBoolean(settings.general_enable_registration, false),
|
||||||
maintenance_mode: toBoolean(settings.general_maintenance_mode, false),
|
maintenance_mode: toBoolean(settings.general_maintenance_mode, false),
|
||||||
|
|||||||
@@ -161,28 +161,6 @@ export const GeneralTab: React.FC<GeneralTabProps> = ({
|
|||||||
{t('settings.general.maxFilesPerUploadHelp', { max: MAX_FILES_PER_UPLOAD_LIMIT })}
|
{t('settings.general.maxFilesPerUploadHelp', { max: MAX_FILES_PER_UPLOAD_LIMIT })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
|
||||||
{t('settings.general.maxUploadBatchSize')}
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={generalSettings.max_upload_batch_size_mb}
|
|
||||||
onChange={(e) => {
|
|
||||||
const parsed = parseInt(e.target.value, 10);
|
|
||||||
setGeneralSettings(prev => ({
|
|
||||||
...prev,
|
|
||||||
max_upload_batch_size_mb: Number.isFinite(parsed)
|
|
||||||
? Math.max(1, parsed)
|
|
||||||
: prev.max_upload_batch_size_mb
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
|
||||||
{t('settings.general.maxUploadBatchSizeHelp')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1012,8 +1012,6 @@
|
|||||||
"maxFileSizeHelp": "Maximale Größe pro hochgeladenem Foto",
|
"maxFileSizeHelp": "Maximale Größe pro hochgeladenem Foto",
|
||||||
"maxFilesPerUpload": "Max. Dateien pro Upload",
|
"maxFilesPerUpload": "Max. Dateien pro Upload",
|
||||||
"maxFilesPerUploadHelp": "Maximale Anzahl an Fotos pro Upload-Vorgang (1-{{max}}).",
|
"maxFilesPerUploadHelp": "Maximale Anzahl an Fotos pro Upload-Vorgang (1-{{max}}).",
|
||||||
"maxUploadBatchSize": "Max. Upload-Paketgröße (MB)",
|
|
||||||
"maxUploadBatchSizeHelp": "Maximale Größe pro Upload-Anfrage. Reduzieren Sie diesen Wert bei Nutzung eines Reverse-Proxys mit Größenbeschränkung (z.B. Cloudflare: 100MB).",
|
|
||||||
"allowedFileTypes": "Erlaubte Dateitypen",
|
"allowedFileTypes": "Erlaubte Dateitypen",
|
||||||
"allowedFileTypesHelp": "Kommagetrennte Liste von Dateierweiterungen",
|
"allowedFileTypesHelp": "Kommagetrennte Liste von Dateierweiterungen",
|
||||||
"featureToggles": "Funktionsschalter",
|
"featureToggles": "Funktionsschalter",
|
||||||
|
|||||||
@@ -637,8 +637,6 @@
|
|||||||
"maxFileSizeHelp": "Maximum size per uploaded photo",
|
"maxFileSizeHelp": "Maximum size per uploaded photo",
|
||||||
"maxFilesPerUpload": "Max Files per Upload",
|
"maxFilesPerUpload": "Max Files per Upload",
|
||||||
"maxFilesPerUploadHelp": "Maximum number of photos allowed in a single upload batch (1-{{max}}).",
|
"maxFilesPerUploadHelp": "Maximum number of photos allowed in a single upload batch (1-{{max}}).",
|
||||||
"maxUploadBatchSize": "Max Upload Batch Size (MB)",
|
|
||||||
"maxUploadBatchSizeHelp": "Maximum size per upload request. Lower this if behind a reverse proxy with request size limits (e.g. Cloudflare: 100MB).",
|
|
||||||
"allowedFileTypes": "Allowed File Types",
|
"allowedFileTypes": "Allowed File Types",
|
||||||
"allowedFileTypesHelp": "Comma-separated list of file extensions",
|
"allowedFileTypesHelp": "Comma-separated list of file extensions",
|
||||||
"featureToggles": "Feature Toggles",
|
"featureToggles": "Feature Toggles",
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
|
||||||
|
|
||||||
async function getAdminToken(page: import('@playwright/test').Page): Promise<string> {
|
|
||||||
const loginRes = await page.request.post('/api/auth/admin/login', {
|
|
||||||
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
|
||||||
});
|
|
||||||
const body = await loginRes.json();
|
|
||||||
return body.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function adminLogin(page: import('@playwright/test').Page) {
|
|
||||||
await page.goto('/admin/login');
|
|
||||||
await page.getByLabel(/Email|E-Mail/i).fill(ADMIN_EMAIL);
|
|
||||||
await page.getByLabel(/Password|Passwort/i).fill(ADMIN_PASSWORD);
|
|
||||||
await page.getByRole('button', { name: /Sign In|Log in|Anmelden/i }).click();
|
|
||||||
await expect(page.getByRole('heading', { name: /Dashboard|Übersicht/i })).toBeVisible({ timeout: 20000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Upload batch size setting', () => {
|
|
||||||
test('setting exists in DB via API with default value 95', async ({ page }) => {
|
|
||||||
const token = await getAdminToken(page);
|
|
||||||
|
|
||||||
const res = await page.request.get('/api/admin/settings', {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
expect(res.ok()).toBeTruthy();
|
|
||||||
const settings = await res.json();
|
|
||||||
expect(settings.general_max_upload_batch_size_mb).toBe(95);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setting appears in General settings UI and can be changed', async ({ page }, testInfo) => {
|
|
||||||
if (testInfo.project.name === 'mobile-chrome') {
|
|
||||||
test.skip('Settings UI validated on desktop viewport');
|
|
||||||
}
|
|
||||||
|
|
||||||
await adminLogin(page);
|
|
||||||
await page.goto('/admin/settings');
|
|
||||||
|
|
||||||
// Find the batch size input by its nearby label text
|
|
||||||
const batchSizeLabel = page.locator('label', { hasText: /Max Upload Batch Size|Max\. Upload-Paketgröße/i });
|
|
||||||
await expect(batchSizeLabel).toBeVisible({ timeout: 10000 });
|
|
||||||
|
|
||||||
// The input is a sibling within the same container
|
|
||||||
const batchSizeInput = batchSizeLabel.locator('..').locator('input[type="number"]');
|
|
||||||
await expect(batchSizeInput).toBeVisible();
|
|
||||||
await expect(batchSizeInput).toHaveValue('95');
|
|
||||||
|
|
||||||
// Change value to 50
|
|
||||||
await batchSizeInput.fill('50');
|
|
||||||
|
|
||||||
// Save general settings
|
|
||||||
const saveButton = page.getByRole('button', { name: /Save General Settings|Allgemeine Einstellungen speichern/i });
|
|
||||||
await saveButton.click();
|
|
||||||
|
|
||||||
// Wait for success toast
|
|
||||||
await expect(page.locator('.Toastify__toast').filter({ hasText: /(Settings saved|Einstellungen gespeichert)/i })).toBeVisible({ timeout: 10000 });
|
|
||||||
|
|
||||||
// Reload and verify persisted
|
|
||||||
await page.reload();
|
|
||||||
const batchSizeLabelAfter = page.locator('label', { hasText: /Max Upload Batch Size|Max\. Upload-Paketgröße/i });
|
|
||||||
await expect(batchSizeLabelAfter).toBeVisible({ timeout: 10000 });
|
|
||||||
const batchSizeInputAfter = batchSizeLabelAfter.locator('..').locator('input[type="number"]');
|
|
||||||
await expect(batchSizeInputAfter).toHaveValue('50');
|
|
||||||
|
|
||||||
// Revert to default
|
|
||||||
await batchSizeInputAfter.fill('95');
|
|
||||||
await page.getByRole('button', { name: /Save General Settings|Allgemeine Einstellungen speichern/i }).click();
|
|
||||||
await expect(page.locator('.Toastify__toast').filter({ hasText: /(Settings saved|Einstellungen gespeichert)/i })).toBeVisible({ timeout: 10000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setting is used for upload chunking via API', async ({ page }) => {
|
|
||||||
const token = await getAdminToken(page);
|
|
||||||
|
|
||||||
// Set batch size to a small value
|
|
||||||
const updateRes = await page.request.put('/api/admin/settings/general', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: { general_max_upload_batch_size_mb: 10 },
|
|
||||||
});
|
|
||||||
expect(updateRes.ok()).toBeTruthy();
|
|
||||||
|
|
||||||
// Verify the setting was saved
|
|
||||||
const getRes = await page.request.get('/api/admin/settings', {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
expect(getRes.ok()).toBeTruthy();
|
|
||||||
const settings = await getRes.json();
|
|
||||||
expect(settings.general_max_upload_batch_size_mb).toBe(10);
|
|
||||||
|
|
||||||
// Revert to default
|
|
||||||
await page.request.put('/api/admin/settings/general', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: { general_max_upload_batch_size_mb: 95 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user