fix(upload): restore configurable batch-size for reverse proxies (#509)
Regression of #208. PR #214 (commit02a46e0, re-merged at9b7495e) 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.
This commit is contained in:
@@ -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();
|
||||||
|
};
|
||||||
@@ -163,9 +163,14 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
|||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
setUploadIds([]);
|
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_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[][] = [];
|
const chunks: File[][] = [];
|
||||||
|
|
||||||
let currentChunk: File[] = [];
|
let currentChunk: File[] = [];
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ 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;
|
||||||
|
// #509 — re-added after the main-into-beta merge dropped it.
|
||||||
|
max_upload_batch_size_mb: number;
|
||||||
enable_analytics: boolean;
|
enable_analytics: boolean;
|
||||||
enable_registration: boolean;
|
enable_registration: boolean;
|
||||||
maintenance_mode: boolean;
|
maintenance_mode: boolean;
|
||||||
@@ -91,6 +93,7 @@ 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,
|
||||||
@@ -177,6 +180,7 @@ 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),
|
||||||
|
|||||||
@@ -162,6 +162,28 @@ 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>
|
||||||
|
|||||||
@@ -1031,6 +1031,8 @@
|
|||||||
"maxFileSize": "Max. Dateigröße (MB)",
|
"maxFileSize": "Max. Dateigröße (MB)",
|
||||||
"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",
|
||||||
|
|||||||
@@ -670,6 +670,8 @@
|
|||||||
"maxFileSize": "Max File Size (MB)",
|
"maxFileSize": "Max File Size (MB)",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user