From f4b685a5abef8071f49540648bb3f0ae033b0c58 Mon Sep 17 00:00:00 2001 From: Dodothereal <129273127+Dodothereal@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:50:32 +0200 Subject: [PATCH] fix(uploads): allow configured raw formats Assisted-by: Claude Code --- .../services/uploadSettingsFileTypes.test.js | 41 +++++++++++++++++++ backend/src/services/uploadSettings.js | 3 ++ backend/src/utils/fileSecurityUtils.js | 16 +++++++- .../src/utils/__tests__/fileTypes.test.ts | 16 +++++--- frontend/src/utils/fileTypes.ts | 3 ++ 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 backend/__tests__/services/uploadSettingsFileTypes.test.js diff --git a/backend/__tests__/services/uploadSettingsFileTypes.test.js b/backend/__tests__/services/uploadSettingsFileTypes.test.js new file mode 100644 index 00000000..e32a674f --- /dev/null +++ b/backend/__tests__/services/uploadSettingsFileTypes.test.js @@ -0,0 +1,41 @@ +const fs = require('fs'); +const path = require('path'); + +const { + EXTENSION_TO_MIME, + extensionsToMimeTypes, +} = require('../../src/services/uploadSettings'); +const { validateFileType } = require('../../src/utils/fileSecurityUtils'); + +const RAW_AND_HEIF_TYPES = { + dng: 'image/x-adobe-dng', + heic: 'image/heic', + heif: 'image/heif', +}; + +function getFrontendExtensionMap() { + const source = fs.readFileSync( + path.join(__dirname, '../../../frontend/src/utils/fileTypes.ts'), + 'utf8' + ); + const match = source.match(/const EXTENSION_TO_MIME[^=]*= \{([\s\S]*?)\n\};/); + if (!match) throw new Error('Could not find frontend EXTENSION_TO_MIME'); + + return Object.fromEntries( + Array.from(match[1].matchAll(/^(\s*)(\w+): '([^']+)',?$/gm), ([, , extension, mime]) => [extension, mime]) + ); +} + +describe('configured upload file types', () => { + test('supports configured DNG, HEIC, and HEIF uploads', () => { + expect(extensionsToMimeTypes('dng,heic,heif')).toEqual(Object.values(RAW_AND_HEIF_TYPES)); + + for (const [extension, mimeType] of Object.entries(RAW_AND_HEIF_TYPES)) { + expect(validateFileType(`image.${extension}`, mimeType, [mimeType])).toBe(true); + } + }); + + test('uses the same extension-to-MIME map as the frontend', () => { + expect(getFrontendExtensionMap()).toEqual(EXTENSION_TO_MIME); + }); +}); diff --git a/backend/src/services/uploadSettings.js b/backend/src/services/uploadSettings.js index 06319cca..623a9966 100644 --- a/backend/src/services/uploadSettings.js +++ b/backend/src/services/uploadSettings.js @@ -24,6 +24,9 @@ const EXTENSION_TO_MIME = { 'png': 'image/png', 'webp': 'image/webp', 'gif': 'image/gif', + 'dng': 'image/x-adobe-dng', + 'heic': 'image/heic', + 'heif': 'image/heif', 'mp4': 'video/mp4', 'm4v': 'video/mp4', 'webm': 'video/webm', diff --git a/backend/src/utils/fileSecurityUtils.js b/backend/src/utils/fileSecurityUtils.js index 39e59b70..afe4f095 100644 --- a/backend/src/utils/fileSecurityUtils.js +++ b/backend/src/utils/fileSecurityUtils.js @@ -75,6 +75,20 @@ const ALLOWED_IMAGE_TYPES = { { offset: 0, bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] } // GIF89a ] }, + // DNG and HEIF-family files use TIFF/ISO Base Media File Format containers, + // so their extension and declared MIME type are validated together here. + 'image/x-adobe-dng': { + extensions: ['.dng'], + magicNumbers: null + }, + 'image/heic': { + extensions: ['.heic'], + magicNumbers: null + }, + 'image/heif': { + extensions: ['.heif'], + magicNumbers: null + }, 'image/svg+xml': { extensions: ['.svg'], // SVG files are XML-based text files, so we skip magic number validation @@ -191,7 +205,7 @@ function getSafeFilename(originalFilename) { const ext = path.extname(originalFilename).toLowerCase(); // Validate extension - including both image and video extensions - const validExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg', '.ico', '.mp4', '.m4v', '.webm', '.mov', '.avi']; + const validExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.dng', '.heic', '.heif', '.svg', '.ico', '.mp4', '.m4v', '.webm', '.mov', '.avi']; if (!validExtensions.includes(ext)) { throw new Error('Invalid file extension'); } diff --git a/frontend/src/utils/__tests__/fileTypes.test.ts b/frontend/src/utils/__tests__/fileTypes.test.ts index e54ff4f1..b2ca856e 100644 --- a/frontend/src/utils/__tests__/fileTypes.test.ts +++ b/frontend/src/utils/__tests__/fileTypes.test.ts @@ -7,8 +7,11 @@ import { describe('file type settings', () => { it('keeps configured supported extensions for the upload hint', () => { - expect(getSupportedExtensions('jpg, mov, mp4, .webp')).toEqual([ + expect(getSupportedExtensions('jpg, dng, heic, heif, mov, mp4, .webp')).toEqual([ 'jpg', + 'dng', + 'heic', + 'heif', 'mov', 'mp4', 'webp', @@ -16,18 +19,21 @@ describe('file type settings', () => { }); it('uses the same configured types for validation and file selection', () => { - expect(extensionsToMimeTypes('jpg,jpeg,mov,mp4')).toEqual([ + expect(extensionsToMimeTypes('jpg,jpeg,dng,heic,heif,mov,mp4')).toEqual([ 'image/jpeg', + 'image/x-adobe-dng', + 'image/heic', + 'image/heif', 'video/quicktime', 'video/mp4', ]); - expect(extensionsToAcceptString('jpg,jpeg,mov,mp4')).toBe( - 'image/jpeg,video/quicktime,video/mp4' + expect(extensionsToAcceptString('dng,heic,heif')).toBe( + 'image/x-adobe-dng,image/heic,image/heif' ); }); it('falls back to the default formats when no configured types are supported', () => { - expect(getSupportedExtensions('dng,unknown')).toEqual([ + expect(getSupportedExtensions('unknown')).toEqual([ 'jpg', 'jpeg', 'png', diff --git a/frontend/src/utils/fileTypes.ts b/frontend/src/utils/fileTypes.ts index 46091dd8..cd2ff22f 100644 --- a/frontend/src/utils/fileTypes.ts +++ b/frontend/src/utils/fileTypes.ts @@ -7,6 +7,9 @@ const EXTENSION_TO_MIME: Record = { png: 'image/png', webp: 'image/webp', gif: 'image/gif', + dng: 'image/x-adobe-dng', + heic: 'image/heic', + heif: 'image/heif', mp4: 'video/mp4', m4v: 'video/mp4', webm: 'video/webm',