From 98f3c3df4184b6d59b6c6b8e5f11b12b362320af Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 17 May 2026 00:42:22 +0200 Subject: [PATCH] fix(upload): restore configurable batch-size for reverse proxies (#509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression of #208. PR #214 (commit 02a46e0, re-merged at 9b7495e) shipped the configurable `general_max_upload_batch_size_mb` setting so users behind Cloudflare Tunnel and other reverse proxies with per-request size caps could lower the chunked-upload size below their proxy's limit. Six days later the "Merge main into beta for release/beta-to-main" commit (28793bb) resolved its conflict by keeping main's older tree — which silently deleted the migration (072), the setting input on Settings → General, the i18n strings, the `useSettingsState` field, and the read in PhotoUpload.tsx, putting the hardcoded 500MB chunk back. Galleries fronted by Cloudflare have quietly been broken on batch uploads since then. Re-applying exactly the same change set: - `backend/migrations/core/072_add_max_upload_batch_size.js` recreated, with a comment pointing at the regression in case the same merge accident happens again. - `frontend/src/components/admin/PhotoUpload.tsx` line 168 now reads the setting from query cache and falls back to 95MB (Cloudflare-safe headroom under 100MB). - `useSettingsState.ts`, `GeneralTab.tsx`, `en.json`, `de.json` — added the field to the state type + defaults + load path + the Site-Configuration input. Existing installs are safe either way: - Ran original 072 then lost the file: migrations table still has the filename, so the runner skips re-applying. The setting row in `app_settings` is also untouched (the deletion was source-only, no down migration ran). Now the new code starts reading it again. - Installed after the regression: migrations runner picks up the new 072 normally and seeds the setting at 95. --- .../core/072_add_max_upload_batch_size.js | 38 +++++++++++++++++++ frontend/src/components/admin/PhotoUpload.tsx | 9 ++++- .../settings/hooks/useSettingsState.ts | 4 ++ .../src/features/settings/tabs/GeneralTab.tsx | 22 +++++++++++ frontend/src/i18n/locales/de.json | 2 + frontend/src/i18n/locales/en.json | 2 + 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/core/072_add_max_upload_batch_size.js diff --git a/backend/migrations/core/072_add_max_upload_batch_size.js b/backend/migrations/core/072_add_max_upload_batch_size.js new file mode 100644 index 00000000..abd381d9 --- /dev/null +++ b/backend/migrations/core/072_add_max_upload_batch_size.js @@ -0,0 +1,38 @@ +/** + * Migration: re-add the configurable upload batch size setting (#509). + * + * Originally shipped via PR #214 (#208 fix) — users behind Cloudflare + * Tunnel and other reverse proxies with per-request size caps need to + * bound the chunked-upload size so they don't lose every batch >100MB. + * That migration + frontend wiring was lost during a `Merge main into + * beta for release/beta-to-main` resolution that picked main's older + * tree over beta's, silently deleting the file and reinstating the + * hardcoded 500MB chunk in PhotoUpload.tsx. + * + * Re-introducing the exact same migration here. Idempotent: skips the + * insert if the row already exists (e.g. installs that did go through + * the original 072 between #214 merge and the main-into-beta merge, + * where the migrations-table row was preserved even after the file + * was deleted). + */ + +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(); +}; diff --git a/frontend/src/components/admin/PhotoUpload.tsx b/frontend/src/components/admin/PhotoUpload.tsx index f193c6a2..2cae80c8 100644 --- a/frontend/src/components/admin/PhotoUpload.tsx +++ b/frontend/src/components/admin/PhotoUpload.tsx @@ -163,9 +163,14 @@ export const PhotoUpload: React.FC = ({ eventId, onUploadCompl setUploadProgress(0); setUploadIds([]); - // 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. + // #509: the per-chunk byte cap MUST be tunable so users behind Cloudflare Tunnel and other + // reverse proxies with request-size limits can drop it below their proxy's cap. Falls back + // to 95MB (Cloudflare-safe headroom under 100MB) when the setting is unset — that matches + // the value the migration seeds and is what worked in #208's resolution. const MAX_FILES_PER_CHUNK = Math.max(1, Math.min(50, maxFilesPerUpload)); // Max 50 files per chunk - const MAX_BYTES_PER_CHUNK = 500 * 1024 * 1024; // Max 500MB per chunk (nginx limit is 1GB) + const maxBatchSizeMb = Number(settings?.general_max_upload_batch_size_mb) || 95; + const MAX_BYTES_PER_CHUNK = maxBatchSizeMb * 1024 * 1024; const chunks: File[][] = []; let currentChunk: File[] = []; diff --git a/frontend/src/features/settings/hooks/useSettingsState.ts b/frontend/src/features/settings/hooks/useSettingsState.ts index a1ad55c7..420098f6 100644 --- a/frontend/src/features/settings/hooks/useSettingsState.ts +++ b/frontend/src/features/settings/hooks/useSettingsState.ts @@ -16,6 +16,8 @@ export interface GeneralSettings { max_file_size_mb: number; max_files_per_upload: number; allowed_file_types: string; + // #509 — re-added after the main-into-beta merge dropped it. + max_upload_batch_size_mb: number; enable_analytics: boolean; enable_registration: boolean; maintenance_mode: boolean; @@ -91,6 +93,7 @@ export function useSettingsState() { max_file_size_mb: 50, max_files_per_upload: 500, allowed_file_types: 'jpg,jpeg,png,gif,webp', + max_upload_batch_size_mb: 95, enable_analytics: true, enable_registration: false, maintenance_mode: false, @@ -177,6 +180,7 @@ export function useSettingsState() { Math.max(1, toNumber(settings.general_max_files_per_upload, 500)) ), 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_registration: toBoolean(settings.general_enable_registration, false), maintenance_mode: toBoolean(settings.general_maintenance_mode, false), diff --git a/frontend/src/features/settings/tabs/GeneralTab.tsx b/frontend/src/features/settings/tabs/GeneralTab.tsx index 8c7bd491..ba27bc3e 100644 --- a/frontend/src/features/settings/tabs/GeneralTab.tsx +++ b/frontend/src/features/settings/tabs/GeneralTab.tsx @@ -162,6 +162,28 @@ export const GeneralTab: React.FC = ({ {t('settings.general.maxFilesPerUploadHelp', { max: MAX_FILES_PER_UPLOAD_LIMIT })}

+
+ + { + 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" + /> +

+ {t('settings.general.maxUploadBatchSizeHelp')} +

+
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 63925399..6524a6aa 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1031,6 +1031,8 @@ "maxFileSize": "Max. Dateigröße (MB)", "maxFilesPerUpload": "Max. Dateien pro Upload", "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", "allowedFileTypesHelp": "Kommagetrennte Liste von Dateierweiterungen", "featureToggles": "Funktionsschalter", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index e5948b34..4a686dd8 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -670,6 +670,8 @@ "maxFileSize": "Max File Size (MB)", "maxFilesPerUpload": "Max Files per Upload", "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", "allowedFileTypesHelp": "Comma-separated list of file extensions", "featureToggles": "Feature Toggles",