import { createLogger } from '@sim/logger' import { uploadExecutionFile } from '@/lib/uploads/contexts/execution' import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' import type { InputFormatField } from '@/lib/workflows/types' import type { UserFile } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' const logger = createLogger('ExecutionFiles') const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB /** * Process a single file for workflow execution - handles base64 ('file' type) and URL downloads ('url' type) */ export async function processExecutionFile( file: { type: string; data: string; name: string; mime?: string }, executionContext: { workspaceId: string; workflowId: string; executionId: string }, requestId: string, userId?: string ): Promise { if (file.type === 'file' && file.data && file.name) { const dataUrlPrefix = 'data:' const base64Prefix = ';base64,' if (!file.data.startsWith(dataUrlPrefix)) { logger.warn(`[${requestId}] Invalid data format for file: ${file.name}`) return null } const base64Index = file.data.indexOf(base64Prefix) if (base64Index === -1) { logger.warn(`[${requestId}] Invalid data format (no base64 marker) for file: ${file.name}`) return null } const mimeType = file.data.substring(dataUrlPrefix.length, base64Index) const base64Data = file.data.substring(base64Index + base64Prefix.length) const buffer = Buffer.from(base64Data, 'base64') if (buffer.length > MAX_FILE_SIZE) { const fileSizeMB = (buffer.length / (1024 * 1024)).toFixed(2) throw new Error( `File "${file.name}" exceeds the maximum size limit of 20MB (actual size: ${fileSizeMB}MB)` ) } const userFile = await uploadExecutionFile( executionContext, buffer, file.name, mimeType || file.mime || 'application/octet-stream', userId ) return userFile } if (file.type === 'url' && file.data) { const { downloadFileFromUrl } = await import('@/lib/uploads/utils/file-utils.server') const buffer = await downloadFileFromUrl(file.data, { userId }) if (buffer.length > MAX_FILE_SIZE) { const fileSizeMB = (buffer.length / (1024 * 1024)).toFixed(2) throw new Error( `File "${file.name}" exceeds the maximum size limit of 20MB (actual size: ${fileSizeMB}MB)` ) } const userFile = await uploadExecutionFile( executionContext, buffer, file.name, file.mime || 'application/octet-stream', userId ) return userFile } return null } /** * Process all files for a given field in workflow execution input */ export async function processExecutionFiles( fieldValue: any, executionContext: { workspaceId: string; workflowId: string; executionId: string }, requestId: string, userId?: string ): Promise { if (!fieldValue || typeof fieldValue !== 'object') { return [] } const files = Array.isArray(fieldValue) ? fieldValue : [fieldValue] const uploadedFiles: UserFile[] = [] const fullContext = { ...executionContext } for (const file of files) { try { const userFile = await processExecutionFile(file, fullContext, requestId, userId) if (userFile) { uploadedFiles.push(userFile) } } catch (error) { logger.error(`[${requestId}] Failed to process file ${file.name}:`, error) throw new Error(`Failed to upload file: ${file.name}`) } } return uploadedFiles } /** * Extract inputFormat fields from a start block or trigger block */ type ValidatedInputFormatField = Required> function extractInputFormatFromBlock(block: SerializedBlock): ValidatedInputFormatField[] { const metadata = block.metadata as { subBlocks?: Record } | undefined const subBlocksValue = metadata?.subBlocks?.inputFormat?.value const legacyValue = block.config?.params?.inputFormat const inputFormatValue = subBlocksValue ?? legacyValue if (!Array.isArray(inputFormatValue) || inputFormatValue.length === 0) { return [] } return inputFormatValue.filter( (field): field is ValidatedInputFormatField => field && typeof field === 'object' && 'name' in field && 'type' in field && typeof field.name === 'string' && typeof field.type === 'string' ) } /** * Process file fields in workflow input based on the start block's inputFormat * This handles base64 and URL file inputs from API calls */ export async function processInputFileFields( input: unknown, blocks: SerializedBlock[], executionContext: { workspaceId: string; workflowId: string; executionId: string }, requestId: string, userId?: string ): Promise { if (!input || typeof input !== 'object' || blocks.length === 0) { return input } const startBlock = blocks.find((block) => { const blockType = block.metadata?.id return ( blockType === TRIGGER_TYPES.START || blockType === TRIGGER_TYPES.API || blockType === TRIGGER_TYPES.INPUT || blockType === TRIGGER_TYPES.GENERIC_WEBHOOK || blockType === TRIGGER_TYPES.STARTER ) }) if (!startBlock) { return input } const inputFormat = extractInputFormatFromBlock(startBlock) const fileFields = inputFormat.filter((field) => field.type === 'file[]') if (fileFields.length === 0) { return input } const processedInput = { ...input } as Record for (const fileField of fileFields) { const fieldValue = processedInput[fileField.name] if (fieldValue && typeof fieldValue === 'object') { const uploadedFiles = await processExecutionFiles( fieldValue, executionContext, requestId, userId ) if (uploadedFiles.length > 0) { processedInput[fileField.name] = uploadedFiles logger.info( `[${requestId}] Successfully processed ${uploadedFiles.length} file(s) for field: ${fileField.name}` ) } } } return processedInput }