fix(uploads): apply configured max file size to guest uploads (#613 follow-up)
The admin's Settings → General → "Max File Size (MB)" value (general_max_file_size_mb) never applied to guest gallery uploads — the guest route hardcoded multer's per-file cap at 50MB (gallery.js) and the guest UI hardcoded the same 50MB client-side guard and "max 50MB" hint text. So a guest could not upload a large video even when the admin raised the limit (reported by mat1990dj on #613). Same class as the file-count miss fixed in #614, for size. - uploadSettings.js: new getMaxFileSizeMb()/getMaxFileSizeBytes() reading general_max_file_size_mb (default 50MB, cached 60s, clamped to a 10GB ceiling), mirroring getMaxFilesPerUpload. - gallery.js (guest upload): multer limits.fileSize now resolves from the setting; a LIMIT_FILE_SIZE error returns an actionable "max N MB" message. - publicSettings.js: exposes general_max_file_size_mb (default 50) so the gallery UI can render the real limit and guard client-side before an oversized POST. - UserPhotoUpload.tsx: reads the limit, uses it for the client-side size guard, and passes it to the requirements hint. The "max 50MB" literal in upload.fileRequirements is now interpolated ({{sizeLimit}}) across all 8 locales; adds upload.fileTooLarge (en/de; others fall back to en). Scope: guest path only (the reported gap). The admin path keeps its generous 10GB cap — admins are trusted and default 50MB would otherwise regress large admin video uploads. Format and batch-size limits already work correctly and are untouched. Adds SQLite-backed unit tests for the new getter. Verified end-to-end on a booted instance: admin sets 500MB → persisted → public settings exposes 500 → guest multer sources its cap from it.
This commit is contained in:
@@ -1833,7 +1833,7 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
||||
|
||||
// Import multer and photo processing
|
||||
const multer = require('multer');
|
||||
const { getAllowedMimeTypes, getMaxFilesPerUpload } = require('../services/uploadSettings');
|
||||
const { getAllowedMimeTypes, getMaxFilesPerUpload, getMaxFileSizeBytes, DEFAULT_MAX_FILE_SIZE_MB } = require('../services/uploadSettings');
|
||||
const { validateFileType } = require('../utils/fileSecurityUtils');
|
||||
|
||||
// Resolve allowed MIME types from settings
|
||||
@@ -1859,10 +1859,22 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
||||
maxFilesPerUpload = 500;
|
||||
}
|
||||
|
||||
// Per-file size cap was hardcoded to 50MB here, so the admin's Settings →
|
||||
// General → "Max File Size (MB)" value (general_max_file_size_mb) never
|
||||
// applied to guest uploads — a guest could not upload a large video even
|
||||
// when the admin allowed it (reported on #613 by mat1990dj). Resolve it from
|
||||
// settings like the count above; fall back to the 50MB default on read error.
|
||||
let maxFileSizeBytes;
|
||||
try {
|
||||
maxFileSizeBytes = await getMaxFileSizeBytes();
|
||||
} catch {
|
||||
maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
dest: tempUploadDir,
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB per file (separate concern from #613)
|
||||
fileSize: maxFileSizeBytes,
|
||||
files: maxFilesPerUpload
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
@@ -1878,6 +1890,12 @@ router.post('/:eventId/upload', verifyGalleryAccess, denySlideshowToken, async (
|
||||
upload(req, res, async (err) => {
|
||||
if (err) {
|
||||
logger.error('Upload error:', err);
|
||||
// Turn multer's generic "File too large" into an actionable message
|
||||
// that names the configured limit.
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
const limitMb = Math.floor(maxFileSizeBytes / (1024 * 1024));
|
||||
return res.status(400).json({ error: `File too large. Maximum size is ${limitMb} MB per file.` });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ router.get('/', async (req, res) => {
|
||||
// but a client-side guard saves a 4MB+ round-trip when the
|
||||
// limit is small.
|
||||
'general_max_files_per_upload',
|
||||
// Same rationale for the per-file size limit — the gallery upload
|
||||
// component renders it in the requirements hint and guards
|
||||
// client-side before posting an oversized file. Backend enforces
|
||||
// via getMaxFileSizeBytes regardless.
|
||||
'general_max_file_size_mb',
|
||||
// #798 — the admin login page needs to know whether to show
|
||||
// the "Sign in with SSO" button (and its label). Only these
|
||||
// two oidc_* keys are public; issuer/client stay admin-only.
|
||||
@@ -207,6 +212,12 @@ router.get('/', async (req, res) => {
|
||||
general_max_files_per_upload: Number.isFinite(Number(settingsObject.general_max_files_per_upload))
|
||||
? Number(settingsObject.general_max_files_per_upload)
|
||||
: 500,
|
||||
// Per-file size limit (MB). Default mirrors uploadSettings.js
|
||||
// DEFAULT_MAX_FILE_SIZE_MB so the gallery UI shows a sensible number on
|
||||
// installs that never set it explicitly.
|
||||
general_max_file_size_mb: Number.isFinite(Number(settingsObject.general_max_file_size_mb))
|
||||
? Number(settingsObject.general_max_file_size_mb)
|
||||
: 50,
|
||||
// SEO meta tag flags (safe to expose - these are intended for crawlers)
|
||||
seo_meta_noindex: settingsObject.seo_meta_noindex === true,
|
||||
seo_meta_nofollow: settingsObject.seo_meta_nofollow === true,
|
||||
|
||||
@@ -5,9 +5,18 @@ const DEFAULT_MAX_FILES_PER_UPLOAD = 500;
|
||||
const MAX_ALLOWED_FILES_PER_UPLOAD = 2000;
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
|
||||
// Per-file upload size limit (general_max_file_size_mb). The admin sets this in
|
||||
// Settings → General; the default mirrors the frontend's default (50 MB). A
|
||||
// hard ceiling keeps a fat-fingered value from disabling multer's guard.
|
||||
const DEFAULT_MAX_FILE_SIZE_MB = 50;
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 10 * 1024; // 10 GB — matches the admin path's cap
|
||||
|
||||
let cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD;
|
||||
let cacheExpiresAt = 0;
|
||||
|
||||
let cachedFileSizeMb = DEFAULT_MAX_FILE_SIZE_MB;
|
||||
let fileSizeCacheExpiresAt = 0;
|
||||
|
||||
// Map of file extension to MIME type(s)
|
||||
const EXTENSION_TO_MIME = {
|
||||
'jpg': 'image/jpeg',
|
||||
@@ -99,6 +108,52 @@ const clearMaxFilesPerUploadCache = () => {
|
||||
cacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
const normalizeFileSizeMb = (value) => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||
return DEFAULT_MAX_FILE_SIZE_MB;
|
||||
}
|
||||
const intValue = Math.floor(value);
|
||||
if (intValue < 1) return DEFAULT_MAX_FILE_SIZE_MB;
|
||||
if (intValue > MAX_ALLOWED_FILE_SIZE_MB) return MAX_ALLOWED_FILE_SIZE_MB;
|
||||
return intValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-file upload size limit in MB (general_max_file_size_mb). Cached 60s, same
|
||||
* as the other upload settings. Falls back to the default on a read error.
|
||||
*/
|
||||
const getMaxFileSizeMb = async () => {
|
||||
if (Date.now() < fileSizeCacheExpiresAt) {
|
||||
return cachedFileSizeMb;
|
||||
}
|
||||
|
||||
try {
|
||||
const setting = await db('app_settings')
|
||||
.where({ setting_key: 'general_max_file_size_mb' })
|
||||
.first();
|
||||
|
||||
const parsedValue = normalizeFileSizeMb(parseSettingValue(setting));
|
||||
cachedFileSizeMb = parsedValue;
|
||||
fileSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||
return parsedValue;
|
||||
} catch (error) {
|
||||
logger.error('Failed to read max file size setting:', error.message);
|
||||
cachedFileSizeMb = DEFAULT_MAX_FILE_SIZE_MB;
|
||||
fileSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||
return DEFAULT_MAX_FILE_SIZE_MB;
|
||||
}
|
||||
};
|
||||
|
||||
/** Per-file upload size limit in bytes — convenience for multer `limits.fileSize`. */
|
||||
const getMaxFileSizeBytes = async () => {
|
||||
const mb = await getMaxFileSizeMb();
|
||||
return mb * 1024 * 1024;
|
||||
};
|
||||
|
||||
const clearMaxFileSizeCache = () => {
|
||||
fileSizeCacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a comma-separated list of file extensions into an array of MIME types.
|
||||
* Unknown extensions are silently ignored.
|
||||
@@ -164,11 +219,16 @@ const clearAllowedTypesCache = () => {
|
||||
module.exports = {
|
||||
getMaxFilesPerUpload,
|
||||
clearMaxFilesPerUploadCache,
|
||||
getMaxFileSizeMb,
|
||||
getMaxFileSizeBytes,
|
||||
clearMaxFileSizeCache,
|
||||
getAllowedMimeTypes,
|
||||
clearAllowedTypesCache,
|
||||
extensionsToMimeTypes,
|
||||
EXTENSION_TO_MIME,
|
||||
DEFAULT_MAX_FILES_PER_UPLOAD,
|
||||
MAX_ALLOWED_FILES_PER_UPLOAD,
|
||||
DEFAULT_MAX_FILE_SIZE_MB,
|
||||
MAX_ALLOWED_FILE_SIZE_MB,
|
||||
DEFAULT_ALLOWED_FILE_TYPES
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user