fix(upload): scope category ids, stop temp-file leaks, split the video cap
Four related fixes on the admin upload/photo path. B5 -- PATCH /photos/:photoId and POST /photos/bulk-update took any parseInt(...) > 0 straight into the update with no existence or scope check, so a photo could be moved into another event's category. The upload route already validated `event_id = X OR is_global` per #500/#525; extracted that query as findScopedCategory() and used it on all three routes so the 400 body is byte-identical. 0/negative/'individual'/'collage'/null still clear without a lookup, so the clear path costs no extra query. B9 -- three distinct temp-file leaks, not one. The validator's size branch never unlinked; the cleanup lived in the final handler, unreachable on any 400; and multer's `destination` callback runs per file and overwrote req.tempUploadPath, so even the success path only ever removed the last file's directory. Now: discardUploadedFiles() runs on every 4xx and the 500 (ENOENT tolerated, and files are only dropped when the whole request is being rejected, so the passing path is untouched); cleanup registered before multer so it also covers multer's own LIMIT_FILE_SIZE return; one directory per request. B8 -- the admin uploader filtered on MIME only, so an oversized file was uploaded in full before the server's 400. Mirrors UserPhotoUpload's existing per-file toast-and-drop. C4 -- general_max_file_size_mb was a single cap for photos and videos, so the 50MB default meant admins could not upload ordinary video without also raising the photo limit. Adds general_max_video_size_mb (default 500MB, clamped by the same 10GB MAX_ALLOWED_FILE_SIZE_MB ceiling, read per request, 60s cache), editable in Settings -> General. Photo uploads are protected from regressing by keeping multer's type-blind limit at max(photoCap, videoCap) and moving the per-kind decision into validateUploadContent, where file.mimetype exists. It 400s with the existing message shape, so an oversized photo is still rejected with the identical body it produced when multer did the rejecting. Known gap: chunked-upload/init still applies the photo cap to video. Making it video-aware would change an existing assertion that pins a 200MB video init being rejected under a 1MB general cap. No component calls that path today and the direction is strict rather than a bypass, so it is left as-is. Guest video uploads still share the single cap in gallery.js. Refs testplan REPORT.md B5, B8, B9, C4.
This commit is contained in:
@@ -11,12 +11,21 @@ const CACHE_TTL_MS = 60_000;
|
||||
const DEFAULT_MAX_FILE_SIZE_MB = 50;
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 10 * 1024; // 10 GB — matches the admin path's cap
|
||||
|
||||
// Separate per-file cap for videos (general_max_video_size_mb). A single cap
|
||||
// for both meant a 50 MB photo limit also blocked every normal clip, so an
|
||||
// admin had to raise the photo limit to upload a video. 500 MB is roughly a
|
||||
// few minutes of phone footage; the same 10 GB hard ceiling applies.
|
||||
const DEFAULT_MAX_VIDEO_SIZE_MB = 500;
|
||||
|
||||
let cachedValue = DEFAULT_MAX_FILES_PER_UPLOAD;
|
||||
let cacheExpiresAt = 0;
|
||||
|
||||
let cachedFileSizeMb = DEFAULT_MAX_FILE_SIZE_MB;
|
||||
let fileSizeCacheExpiresAt = 0;
|
||||
|
||||
let cachedVideoSizeMb = DEFAULT_MAX_VIDEO_SIZE_MB;
|
||||
let videoSizeCacheExpiresAt = 0;
|
||||
|
||||
// Map of file extension to MIME type(s)
|
||||
const EXTENSION_TO_MIME = {
|
||||
'jpg': 'image/jpeg',
|
||||
@@ -118,12 +127,12 @@ const clearMaxFilesPerUploadCache = () => {
|
||||
cacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
const normalizeFileSizeMb = (value) => {
|
||||
const normalizeFileSizeMb = (value, fallbackMb = DEFAULT_MAX_FILE_SIZE_MB) => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||
return DEFAULT_MAX_FILE_SIZE_MB;
|
||||
return fallbackMb;
|
||||
}
|
||||
const intValue = Math.floor(value);
|
||||
if (intValue < 1) return DEFAULT_MAX_FILE_SIZE_MB;
|
||||
if (intValue < 1) return fallbackMb;
|
||||
if (intValue > MAX_ALLOWED_FILE_SIZE_MB) return MAX_ALLOWED_FILE_SIZE_MB;
|
||||
return intValue;
|
||||
};
|
||||
@@ -164,6 +173,43 @@ const clearMaxFileSizeCache = () => {
|
||||
fileSizeCacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-file upload size limit for videos in MB (general_max_video_size_mb).
|
||||
* Same read/cache/clamp contract as getMaxFileSizeMb(); falls back to the
|
||||
* video default (not the photo one) when the setting is absent.
|
||||
*/
|
||||
const getMaxVideoSizeMb = async () => {
|
||||
if (Date.now() < videoSizeCacheExpiresAt) {
|
||||
return cachedVideoSizeMb;
|
||||
}
|
||||
|
||||
try {
|
||||
const setting = await db('app_settings')
|
||||
.where({ setting_key: 'general_max_video_size_mb' })
|
||||
.first();
|
||||
|
||||
const parsedValue = normalizeFileSizeMb(parseSettingValue(setting), DEFAULT_MAX_VIDEO_SIZE_MB);
|
||||
cachedVideoSizeMb = parsedValue;
|
||||
videoSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||
return parsedValue;
|
||||
} catch (error) {
|
||||
logger.error('Failed to read max video size setting:', error.message);
|
||||
cachedVideoSizeMb = DEFAULT_MAX_VIDEO_SIZE_MB;
|
||||
videoSizeCacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||
return DEFAULT_MAX_VIDEO_SIZE_MB;
|
||||
}
|
||||
};
|
||||
|
||||
/** Per-file video size limit in bytes — convenience for multer `limits.fileSize`. */
|
||||
const getMaxVideoSizeBytes = async () => {
|
||||
const mb = await getMaxVideoSizeMb();
|
||||
return mb * 1024 * 1024;
|
||||
};
|
||||
|
||||
const clearMaxVideoSizeCache = () => {
|
||||
videoSizeCacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a comma-separated list of file extensions into an array of MIME types.
|
||||
* Unknown extensions are silently ignored.
|
||||
@@ -232,6 +278,9 @@ module.exports = {
|
||||
getMaxFileSizeMb,
|
||||
getMaxFileSizeBytes,
|
||||
clearMaxFileSizeCache,
|
||||
getMaxVideoSizeMb,
|
||||
getMaxVideoSizeBytes,
|
||||
clearMaxVideoSizeCache,
|
||||
getAllowedMimeTypes,
|
||||
clearAllowedTypesCache,
|
||||
extensionsToMimeTypes,
|
||||
@@ -239,6 +288,7 @@ module.exports = {
|
||||
DEFAULT_MAX_FILES_PER_UPLOAD,
|
||||
MAX_ALLOWED_FILES_PER_UPLOAD,
|
||||
DEFAULT_MAX_FILE_SIZE_MB,
|
||||
DEFAULT_MAX_VIDEO_SIZE_MB,
|
||||
MAX_ALLOWED_FILE_SIZE_MB,
|
||||
DEFAULT_ALLOWED_FILE_TYPES
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user