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 })}