mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
📂 refactor: File Type Inference for Frontend File Validation (#10807)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
- Introduced `inferMimeType` utility to improve MIME type detection for uploaded files, including support for HEIC and HEIF formats. - Updated DragDropModal to utilize the new inference logic for validating file types, ensuring compatibility with various document upload providers. - Added comprehensive tests for `inferMimeType` to cover various scenarios, including handling of unknown extensions and preserving browser-provided types.
This commit is contained in:
parent
754b495fb8
commit
f55bd6f99b
4 changed files with 101 additions and 19 deletions
|
|
@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { OGDialog, OGDialogTemplate } from '@librechat/client';
|
import { OGDialog, OGDialogTemplate } from '@librechat/client';
|
||||||
import {
|
import {
|
||||||
|
inferMimeType,
|
||||||
EToolResources,
|
EToolResources,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
defaultAgentCapabilities,
|
defaultAgentCapabilities,
|
||||||
|
|
@ -56,18 +57,26 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
|
||||||
const _options: FileOption[] = [];
|
const _options: FileOption[] = [];
|
||||||
const currentProvider = provider || endpoint;
|
const currentProvider = provider || endpoint;
|
||||||
|
|
||||||
|
/** Helper to get inferred MIME type for a file */
|
||||||
|
const getFileType = (file: File) => inferMimeType(file.name, file.type);
|
||||||
|
|
||||||
// Check if provider supports document upload
|
// Check if provider supports document upload
|
||||||
if (isDocumentSupportedProvider(endpointType) || isDocumentSupportedProvider(currentProvider)) {
|
if (isDocumentSupportedProvider(endpointType) || isDocumentSupportedProvider(currentProvider)) {
|
||||||
const isGoogleProvider = currentProvider === EModelEndpoint.google;
|
const isGoogleProvider = currentProvider === EModelEndpoint.google;
|
||||||
const validFileTypes = isGoogleProvider
|
const validFileTypes = isGoogleProvider
|
||||||
? files.every(
|
? files.every((file) => {
|
||||||
(file) =>
|
const type = getFileType(file);
|
||||||
file.type?.startsWith('image/') ||
|
return (
|
||||||
file.type?.startsWith('video/') ||
|
type?.startsWith('image/') ||
|
||||||
file.type?.startsWith('audio/') ||
|
type?.startsWith('video/') ||
|
||||||
file.type === 'application/pdf',
|
type?.startsWith('audio/') ||
|
||||||
)
|
type === 'application/pdf'
|
||||||
: files.every((file) => file.type?.startsWith('image/') || file.type === 'application/pdf');
|
);
|
||||||
|
})
|
||||||
|
: files.every((file) => {
|
||||||
|
const type = getFileType(file);
|
||||||
|
return type?.startsWith('image/') || type === 'application/pdf';
|
||||||
|
});
|
||||||
|
|
||||||
_options.push({
|
_options.push({
|
||||||
label: localize('com_ui_upload_provider'),
|
label: localize('com_ui_upload_provider'),
|
||||||
|
|
@ -81,7 +90,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
|
||||||
label: localize('com_ui_upload_image_input'),
|
label: localize('com_ui_upload_image_input'),
|
||||||
value: undefined,
|
value: undefined,
|
||||||
icon: <ImageUpIcon className="icon-md" />,
|
icon: <ImageUpIcon className="icon-md" />,
|
||||||
condition: files.every((file) => file.type?.startsWith('image/')),
|
condition: files.every((file) => getFileType(file)?.startsWith('image/')),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (capabilities.fileSearchEnabled && fileSearchAllowedByAgent) {
|
if (capabilities.fileSearchEnabled && fileSearchAllowedByAgent) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { EModelEndpoint, isDocumentSupportedProvider } from 'librechat-data-provider';
|
import {
|
||||||
|
EModelEndpoint,
|
||||||
|
isDocumentSupportedProvider,
|
||||||
|
inferMimeType,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
|
||||||
describe('DragDropModal - Provider Detection', () => {
|
describe('DragDropModal - Provider Detection', () => {
|
||||||
describe('endpointType priority over currentProvider', () => {
|
describe('endpointType priority over currentProvider', () => {
|
||||||
|
|
@ -118,4 +122,59 @@ describe('DragDropModal - Provider Detection', () => {
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HEIC/HEIF file type inference', () => {
|
||||||
|
it('should infer image/heic for .heic files when browser returns empty type', () => {
|
||||||
|
const fileName = 'photo.heic';
|
||||||
|
const browserType = '';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('image/heic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should infer image/heif for .heif files when browser returns empty type', () => {
|
||||||
|
const fileName = 'photo.heif';
|
||||||
|
const browserType = '';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('image/heif');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle uppercase .HEIC extension', () => {
|
||||||
|
const fileName = 'IMG_1234.HEIC';
|
||||||
|
const browserType = '';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('image/heic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve browser-provided type when available', () => {
|
||||||
|
const fileName = 'photo.jpg';
|
||||||
|
const browserType = 'image/jpeg';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('image/jpeg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override browser type even if extension differs', () => {
|
||||||
|
const fileName = 'renamed.heic';
|
||||||
|
const browserType = 'image/png';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify HEIC as image type for upload options', () => {
|
||||||
|
const heicType = inferMimeType('photo.heic', '');
|
||||||
|
expect(heicType.startsWith('image/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string for unknown extension with no browser type', () => {
|
||||||
|
const fileName = 'file.xyz';
|
||||||
|
const browserType = '';
|
||||||
|
|
||||||
|
const inferredType = inferMimeType(fileName, browserType);
|
||||||
|
expect(inferredType).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import {
|
||||||
import {
|
import {
|
||||||
megabyte,
|
megabyte,
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
|
inferMimeType,
|
||||||
excelMimeTypes,
|
excelMimeTypes,
|
||||||
EToolResources,
|
EToolResources,
|
||||||
codeTypeMapping,
|
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider';
|
import type { TFile, EndpointFileConfig, FileConfig } from 'librechat-data-provider';
|
||||||
|
|
@ -257,14 +257,7 @@ export const validateFiles = ({
|
||||||
|
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
let originalFile = fileList[i];
|
let originalFile = fileList[i];
|
||||||
let fileType = originalFile.type;
|
const fileType = inferMimeType(originalFile.name, originalFile.type);
|
||||||
const extension = originalFile.name.split('.').pop() ?? '';
|
|
||||||
const knownCodeType = codeTypeMapping[extension];
|
|
||||||
|
|
||||||
// Infer MIME type for Known Code files when the type is empty or a mismatch
|
|
||||||
if (knownCodeType && (!fileType || fileType !== knownCodeType)) {
|
|
||||||
fileType = knownCodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file type is still empty after the extension check
|
// Check if the file type is still empty after the extension check
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,27 @@ export const codeTypeMapping: { [key: string]: string } = {
|
||||||
tsv: 'text/tab-separated-values',
|
tsv: 'text/tab-separated-values',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Maps image extensions to MIME types for formats browsers may not recognize */
|
||||||
|
export const imageTypeMapping: { [key: string]: string } = {
|
||||||
|
heic: 'image/heic',
|
||||||
|
heif: 'image/heif',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infers the MIME type from a file's extension when the browser doesn't recognize it
|
||||||
|
* @param fileName - The name of the file including extension
|
||||||
|
* @param currentType - The current MIME type reported by the browser (may be empty)
|
||||||
|
* @returns The inferred MIME type if browser didn't provide one, otherwise the original type
|
||||||
|
*/
|
||||||
|
export function inferMimeType(fileName: string, currentType: string): string {
|
||||||
|
if (currentType) {
|
||||||
|
return currentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
|
||||||
|
return codeTypeMapping[extension] || imageTypeMapping[extension] || currentType;
|
||||||
|
}
|
||||||
|
|
||||||
export const retrievalMimeTypes = [
|
export const retrievalMimeTypes = [
|
||||||
/^(text\/(x-c|x-c\+\+|x-h|html|x-java|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|vtt|xml))$/,
|
/^(text\/(x-c|x-c\+\+|x-h|html|x-java|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|vtt|xml))$/,
|
||||||
/^(application\/(json|pdf|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation)))$/,
|
/^(application\/(json|pdf|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation)))$/,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue