diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index f328f5861e5..424935f5941 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -13,11 +13,12 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import type { StorageContext } from '@/lib/uploads/config' import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { isImageFileType } from '@/lib/uploads/utils/file-utils' +import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils' import { SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_CODE_EXTENSIONS, SUPPORTED_DOCUMENT_EXTENSIONS, + SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_VIDEO_EXTENSIONS, validateFileType, } from '@/lib/uploads/utils/validation' @@ -28,12 +29,10 @@ import { InvalidRequestError, } from '@/app/api/files/utils' -const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'] as const - const ALLOWED_EXTENSIONS = new Set([ ...SUPPORTED_DOCUMENT_EXTENSIONS, ...SUPPORTED_CODE_EXTENSIONS, - ...IMAGE_EXTENSIONS, + ...SUPPORTED_IMAGE_EXTENSIONS, ...SUPPORTED_AUDIO_EXTENSIONS, ...SUPPORTED_VIDEO_EXTENSIONS, ]) @@ -305,10 +304,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => { context === 'profile-pictures' || context === 'workspace-logos' ) { - if (context !== 'copilot' && !isImageFileType(file.type)) { - throw new InvalidRequestError( - `Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for ${context} uploads` + if (context !== 'copilot') { + const mimeType = file.type + const isGenericMime = !mimeType || mimeType === 'application/octet-stream' + const extension = originalName.split('.').pop()?.toLowerCase() ?? '' + const extensionIsImage = (SUPPORTED_IMAGE_EXTENSIONS as readonly string[]).includes( + extension ) + const isImage = isGenericMime ? extensionIsImage : isImageFileType(mimeType) + if (!isImage) { + throw new InvalidRequestError(`Only image files are allowed for ${context} uploads`) + } } if (context === 'workspace-logos') { @@ -344,6 +350,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { logger.info(`Uploading ${context} file: ${originalName}`) + const resolvedContentType = resolveFileType({ type: file.type, name: originalName }) + const timestamp = Date.now() const safeFileName = sanitizeFileName(originalName) const storageKey = `${context}/${timestamp}-${safeFileName}` @@ -362,7 +370,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const fileInfo = await storageService.uploadFile({ file: buffer, fileName: storageKey, - contentType: file.type, + contentType: resolvedContentType, context, preserveKey: true, customKey: storageKey, @@ -379,7 +387,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { key: fileInfo.key, name: originalName, size: buffer.length, - type: file.type, + type: resolvedContentType, }, directUploadSupported: false, } @@ -400,7 +408,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { fileName: originalName, fileKey: fileInfo.key, fileSize: buffer.length, - fileType: file.type, + fileType: resolvedContentType, }, request, }) diff --git a/apps/sim/app/api/memory/[id]/route.ts b/apps/sim/app/api/memory/[id]/route.ts index 2928fd8b42c..9cc7b5ccc08 100644 --- a/apps/sim/app/api/memory/[id]/route.ts +++ b/apps/sim/app/api/memory/[id]/route.ts @@ -86,7 +86,7 @@ export const GET = withRouteHandler(async (request: NextRequest, context: Memory .limit(1) if (memories.length === 0) { - return memoryEnvelopeError('Memory not found', 404) + return NextResponse.json({ success: true, data: null }, { status: 200 }) } const mem = memories[0] diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index b172d8bc5d9..e259a7dad2a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -43,6 +43,7 @@ import { SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_CODE_EXTENSIONS, SUPPORTED_DOCUMENT_EXTENSIONS, + SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_VIDEO_EXTENSIONS, } from '@/lib/uploads/utils/validation' import type { @@ -89,6 +90,7 @@ const SUPPORTED_EXTENSIONS = [ ...SUPPORTED_CODE_EXTENSIONS, ...SUPPORTED_AUDIO_EXTENSIONS, ...SUPPORTED_VIDEO_EXTENSIONS, + ...SUPPORTED_IMAGE_EXTENSIONS, ] as const const ACCEPT_ATTR = SUPPORTED_EXTENSIONS.map((ext) => `.${ext}`).join(',') @@ -125,6 +127,7 @@ function formatFileType(mimeType: string | null, filename: string): string { if (mimeType?.startsWith('audio/')) return 'Audio' if (mimeType?.startsWith('video/')) return 'Video' + if (mimeType?.startsWith('image/')) return 'Image' const ext = getFileExtension(filename) if (ext) return ext.toUpperCase() @@ -246,6 +249,7 @@ export function Files() { if (typeFilter.includes('document') && isSupportedExtension(ext)) return true if (typeFilter.includes('audio') && isAudioFileType(f.type)) return true if (typeFilter.includes('video') && isVideoFileType(f.type)) return true + if (typeFilter.includes('image') && f.type?.startsWith('image/')) return true return false }) } @@ -926,9 +930,14 @@ export function Files() { typeFilter.length === 0 ? 'All' : typeFilter.length === 1 - ? (({ document: 'Documents', audio: 'Audio', video: 'Video' } as Record)[ - typeFilter[0] - ] ?? typeFilter[0]) + ? (( + { + document: 'Documents', + image: 'Images', + audio: 'Audio', + video: 'Video', + } as Record + )[typeFilter[0]] ?? typeFilter[0]) : `${typeFilter.length} selected` const sizeDisplayLabel = @@ -954,6 +963,7 @@ export function Files() { 0) { const typeLabels: Record = { document: 'Documents', + image: 'Images', audio: 'Audio', video: 'Video', } diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index 3dc0d75506b..edb601b9470 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -33,6 +33,13 @@ export const MIME_TYPE_MAPPING: Record = { gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', + bmp: 'image/bmp', + tif: 'image/tiff', + tiff: 'image/tiff', + heic: 'image/heic', + heif: 'image/heif', + avif: 'image/avif', + ico: 'image/x-icon', // Documents pdf: 'application/pdf', @@ -339,6 +365,13 @@ const MIME_TO_EXTENSION: Record = { 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', + 'image/bmp': 'bmp', + 'image/tiff': 'tiff', + 'image/heic': 'heic', + 'image/heif': 'heif', + 'image/avif': 'avif', + 'image/x-icon': 'ico', + 'image/vnd.microsoft.icon': 'ico', // Documents 'application/pdf': 'pdf', diff --git a/apps/sim/lib/uploads/utils/validation.ts b/apps/sim/lib/uploads/utils/validation.ts index f4e45586793..af0a5581fba 100644 --- a/apps/sim/lib/uploads/utils/validation.ts +++ b/apps/sim/lib/uploads/utils/validation.ts @@ -95,13 +95,31 @@ export const SUPPORTED_AUDIO_EXTENSIONS = [ export const SUPPORTED_VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'mkv', 'webm'] as const +export const SUPPORTED_IMAGE_EXTENSIONS = [ + 'png', + 'jpg', + 'jpeg', + 'gif', + 'webp', + 'svg', + 'bmp', + 'tif', + 'tiff', + 'heic', + 'heif', + 'avif', + 'ico', +] as const + export type SupportedDocumentExtension = (typeof SUPPORTED_DOCUMENT_EXTENSIONS)[number] export type SupportedAudioExtension = (typeof SUPPORTED_AUDIO_EXTENSIONS)[number] export type SupportedVideoExtension = (typeof SUPPORTED_VIDEO_EXTENSIONS)[number] +export type SupportedImageExtension = (typeof SUPPORTED_IMAGE_EXTENSIONS)[number] export type SupportedMediaExtension = | SupportedDocumentExtension | SupportedAudioExtension | SupportedVideoExtension + | SupportedImageExtension export const SUPPORTED_MIME_TYPES: Record = { pdf: ['application/pdf', 'application/x-pdf'], @@ -180,14 +198,19 @@ const SUPPORTED_IMAGE_MIME_TYPES = [ 'image/gif', 'image/webp', 'image/svg+xml', + 'image/bmp', + 'image/tiff', + 'image/heic', + 'image/heif', + 'image/avif', + 'image/x-icon', + 'image/vnd.microsoft.icon', ] -const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'] - export const CHAT_ACCEPT_ATTRIBUTE = [ ACCEPT_ATTRIBUTE, ...SUPPORTED_IMAGE_MIME_TYPES, - ...SUPPORTED_IMAGE_EXTENSIONS, + ...SUPPORTED_IMAGE_EXTENSIONS.map((ext) => `.${ext}`), ].join(',') export interface FileValidationError { diff --git a/apps/sim/tools/memory/get.test.ts b/apps/sim/tools/memory/get.test.ts index 0b7177d19d6..862ca367d1d 100644 --- a/apps/sim/tools/memory/get.test.ts +++ b/apps/sim/tools/memory/get.test.ts @@ -36,6 +36,20 @@ describe('memoryGetTool', () => { expect(url).toBe('/api/memory/team%2Fuser%20123?workspaceId=workspace-1') }) + it('returns empty memories when key is not found (null data)', async () => { + const result = await transformResponse( + new Response(JSON.stringify({ success: true, data: null })) + ) + + expect(result).toEqual({ + success: true, + output: { + memories: [], + message: 'No memories found', + }, + }) + }) + it('wraps the exact memory response as a single result', async () => { const result = await transformResponse( new Response(