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",