Skip to content

Commit 95557bd

Browse files
waleedlatif1claude
andcommitted
fix(uploads): resolve .md file upload rejection and deduplicate file type utilities
Browsers report empty or application/octet-stream MIME types for .md files, causing copilot uploads to be rejected. Added resolveFileType() utility that falls back to extension-based MIME resolution at both client and server boundaries. Consolidated duplicate MIME mappings into module-level constants, removed duplicate isImageFileType from copilot module, and replaced hardcoded ALLOWED_EXTENSIONS with composition from shared validation constants. Also switched file attachment previews to use shared getDocumentIcon utility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 079c7ca commit 95557bd

File tree

8 files changed

+177
-172
lines changed

8 files changed

+177
-172
lines changed

apps/sim/app/api/files/presigned/route.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,13 @@ vi.mock('@/lib/uploads/utils/validation', () => ({
7878
validateFileType: mockValidateFileType,
7979
}))
8080

81+
vi.mock('@/lib/uploads/utils/file-utils', () => ({
82+
isImageFileType: mockIsImageFileType,
83+
}))
84+
8185
vi.mock('@/lib/uploads', () => ({
8286
CopilotFiles: {
8387
generateCopilotUploadUrl: mockGenerateCopilotUploadUrl,
84-
isImageFileType: mockIsImageFileType,
8588
},
8689
getStorageProvider: mockGetStorageProviderUploads,
8790
isUsingCloudStorage: mockIsUsingCloudStorageUploads,

apps/sim/app/api/files/presigned/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CopilotFiles } from '@/lib/uploads'
55
import type { StorageContext } from '@/lib/uploads/config'
66
import { USE_BLOB_STORAGE } from '@/lib/uploads/config'
77
import { generatePresignedUploadUrl, hasCloudStorage } from '@/lib/uploads/core/storage-service'
8+
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
89
import { validateFileType } from '@/lib/uploads/utils/validation'
910
import { createErrorResponse } from '@/app/api/files/utils'
1011

@@ -132,7 +133,7 @@ export async function POST(request: NextRequest) {
132133
'Authenticated user session is required for profile picture uploads'
133134
)
134135
}
135-
if (!CopilotFiles.isImageFileType(contentType)) {
136+
if (!isImageFileType(contentType)) {
136137
throw new ValidationError(
137138
'Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for profile picture uploads'
138139
)

apps/sim/app/api/files/upload/route.ts

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,27 @@ import { sanitizeFileName } from '@/executor/constants'
44
import '@/lib/uploads/core/setup.server'
55
import { getSession } from '@/lib/auth'
66
import type { StorageContext } from '@/lib/uploads/config'
7-
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
8-
import { validateFileType } from '@/lib/uploads/utils/validation'
7+
import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
8+
import {
9+
SUPPORTED_AUDIO_EXTENSIONS,
10+
SUPPORTED_DOCUMENT_EXTENSIONS,
11+
SUPPORTED_VIDEO_EXTENSIONS,
12+
validateFileType,
13+
} from '@/lib/uploads/utils/validation'
914
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1015
import {
1116
createErrorResponse,
1217
createOptionsResponse,
1318
InvalidRequestError,
1419
} from '@/app/api/files/utils'
1520

16-
const ALLOWED_EXTENSIONS = new Set([
17-
// Documents
18-
'pdf',
19-
'doc',
20-
'docx',
21-
'txt',
22-
'md',
23-
'csv',
24-
'xlsx',
25-
'xls',
26-
'json',
27-
'yaml',
28-
'yml',
29-
// Images
30-
'png',
31-
'jpg',
32-
'jpeg',
33-
'gif',
34-
// Audio
35-
'mp3',
36-
'm4a',
37-
'wav',
38-
'webm',
39-
'ogg',
40-
'flac',
41-
'aac',
42-
'opus',
43-
// Video
44-
'mp4',
45-
'mov',
46-
'avi',
47-
'mkv',
21+
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'] as const
22+
23+
const ALLOWED_EXTENSIONS = new Set<string>([
24+
...SUPPORTED_DOCUMENT_EXTENSIONS,
25+
...IMAGE_EXTENSIONS,
26+
...SUPPORTED_AUDIO_EXTENSIONS,
27+
...SUPPORTED_VIDEO_EXTENSIONS,
4828
])
4929

5030
function validateFileExtension(filename: string): boolean {
@@ -257,7 +237,8 @@ export async function POST(request: NextRequest) {
257237
const { isSupportedFileType: isCopilotSupported } = await import(
258238
'@/lib/uploads/contexts/copilot/copilot-file-manager'
259239
)
260-
if (!isImageFileType(file.type) && !isCopilotSupported(file.type)) {
240+
const resolvedType = resolveFileType(file)
241+
if (!isImageFileType(resolvedType) && !isCopilotSupported(resolvedType)) {
261242
throw new InvalidRequestError(
262243
'Unsupported file type. Allowed: images, PDF, and text files (TXT, CSV, MD, HTML, JSON, XML).'
263244
)

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5-
import { FileText } from 'lucide-react'
65
import { useParams, useRouter } from 'next/navigation'
76
import { Skeleton } from '@/components/emcn'
87
import { PanelLeft } from '@/components/emcn/icons'
8+
import { getDocumentIcon } from '@/components/icons/document-icons'
99
import { useSession } from '@/lib/auth/auth-client'
1010
import {
1111
LandingPromptStorage,
@@ -45,6 +45,21 @@ function ThinkingIndicator() {
4545
)
4646
}
4747

48+
interface FileAttachmentPillProps {
49+
mediaType: string
50+
filename: string
51+
}
52+
53+
function FileAttachmentPill({ mediaType, filename }: FileAttachmentPillProps) {
54+
const Icon = getDocumentIcon(mediaType, filename)
55+
return (
56+
<div className='flex max-w-[140px] items-center gap-[5px] rounded-[10px] bg-[var(--surface-5)] px-[6px] py-[3px]'>
57+
<Icon className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]' />
58+
<span className='truncate text-[11px] text-[var(--text-secondary)]'>{filename}</span>
59+
</div>
60+
)
61+
}
62+
4863
const SKELETON_LINE_COUNT = 4
4964

5065
function ChatSkeleton({ children }: { children: React.ReactNode }) {
@@ -235,10 +250,10 @@ export function Home({ chatId }: HomeProps = {}) {
235250

236251
return (
237252
<div className='relative flex h-full bg-[var(--bg)]'>
238-
<div className='flex h-full min-w-0 flex-1 flex-col'>
253+
<div className='relative flex h-full min-w-0 flex-1 flex-col'>
239254
<div
240255
ref={scrollContainerRef}
241-
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 py-4'
256+
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-[98px]'
242257
>
243258
<div className='mx-auto max-w-[42rem] space-y-6'>
244259
{messages.map((msg, index) => {
@@ -262,15 +277,11 @@ export function Home({ chatId }: HomeProps = {}) {
262277
/>
263278
</div>
264279
) : (
265-
<div
280+
<FileAttachmentPill
266281
key={att.id}
267-
className='flex max-w-[140px] items-center gap-[5px] rounded-[10px] bg-[var(--surface-5)] px-[6px] py-[3px]'
268-
>
269-
<FileText className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]' />
270-
<span className='truncate text-[11px] text-[var(--text-secondary)]'>
271-
{att.filename}
272-
</span>
273-
</div>
282+
mediaType={att.media_type}
283+
filename={att.filename}
284+
/>
274285
)
275286
})}
276287
</div>
@@ -317,14 +328,17 @@ export function Home({ chatId }: HomeProps = {}) {
317328
</div>
318329
</div>
319330

320-
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
321-
<UserInput
322-
onSubmit={handleSubmit}
323-
isSending={isSending}
324-
onStopGeneration={stopGeneration}
325-
isInitialView={false}
326-
userId={session?.user?.id}
327-
/>
331+
<div className='pointer-events-none absolute right-0 bottom-0 left-0 z-10 px-[24px] pb-[16px]'>
332+
<div className='pointer-events-auto relative mx-auto max-w-[42rem]'>
333+
<div className='-top-px -right-px -left-px -bottom-[16px] -z-10 absolute rounded-t-[20px] bg-[var(--bg)]' />
334+
<UserInput
335+
onSubmit={handleSubmit}
336+
isSending={isSending}
337+
onStopGeneration={stopGeneration}
338+
isInitialView={false}
339+
userId={session?.user?.id}
340+
/>
341+
</div>
328342
</div>
329343
</div>
330344

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { resolveFileType } from '@/lib/uploads/utils/file-utils'
56

67
const logger = createLogger('useFileAttachments')
78

@@ -117,11 +118,13 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
117118
previewUrl = URL.createObjectURL(file)
118119
}
119120

121+
const resolvedType = resolveFileType(file)
122+
120123
const tempFile: AttachedFile = {
121124
id: crypto.randomUUID(),
122125
name: file.name,
123126
size: file.size,
124-
type: file.type,
127+
type: resolvedType,
125128
path: '',
126129
uploading: true,
127130
previewUrl,

apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
generatePresignedDownloadUrl,
66
generatePresignedUploadUrl,
77
} from '@/lib/uploads/core/storage-service'
8+
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
89
import type { PresignedUrlResponse } from '@/lib/uploads/shared/types'
910

1011
const logger = createLogger('CopilotFileManager')
@@ -29,19 +30,12 @@ const SUPPORTED_FILE_TYPES = [
2930
]
3031

3132
/**
32-
* Check if a file type is supported for copilot attachments
33+
* Check if a MIME type is supported for copilot attachments
3334
*/
3435
export function isSupportedFileType(mimeType: string): boolean {
3536
return SUPPORTED_FILE_TYPES.includes(mimeType.toLowerCase())
3637
}
3738

38-
/**
39-
* Check if a content type is an image
40-
*/
41-
export function isImageFileType(contentType: string): boolean {
42-
return contentType.toLowerCase().startsWith('image/')
43-
}
44-
4539
export interface CopilotFileAttachment {
4640
key: string
4741
filename: string

apps/sim/lib/uploads/contexts/copilot/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export {
55
type GenerateCopilotUploadUrlOptions,
66
generateCopilotDownloadUrl,
77
generateCopilotUploadUrl,
8-
isImageFileType,
98
isSupportedFileType,
109
processCopilotAttachments,
1110
} from './copilot-file-manager'

0 commit comments

Comments
 (0)