diff --git a/sim/app/api/files/presigned/route.ts b/sim/app/api/files/presigned/route.ts new file mode 100644 index 0000000000..a2f888f8f8 --- /dev/null +++ b/sim/app/api/files/presigned/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console-logger' +import { PutObjectCommand } from '@aws-sdk/client-s3' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' +import { s3Client } from '@/lib/uploads/s3-client' +import { S3_CONFIG, USE_S3_STORAGE } from '@/lib/uploads/setup' +import { createErrorResponse, createOptionsResponse } from '../utils' +import { v4 as uuidv4 } from 'uuid' + +const logger = createLogger('PresignedUploadAPI') + +interface PresignedUrlRequest { + fileName: string + contentType: string + fileSize: number +} + +export async function POST(request: NextRequest) { + try { + // Parse the request body + const data: PresignedUrlRequest = await request.json() + const { fileName, contentType, fileSize } = data + + if (!fileName || !contentType) { + return NextResponse.json({ error: 'Missing fileName or contentType' }, { status: 400 }) + } + + // Only proceed if S3 storage is enabled + if (!USE_S3_STORAGE) { + return NextResponse.json({ + error: 'Direct uploads are only available when S3 storage is enabled', + directUploadSupported: false + }, { status: 400 }) + } + + // Create a unique key for the file + const safeFileName = fileName.replace(/\s+/g, '-') + const uniqueKey = `${Date.now()}-${uuidv4()}-${safeFileName}` + + // Create the S3 command + const command = new PutObjectCommand({ + Bucket: S3_CONFIG.bucket, + Key: uniqueKey, + ContentType: contentType, + Metadata: { + originalName: fileName, + uploadedAt: new Date().toISOString() + } + }) + + // Generate the presigned URL + const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }) + + // Create a path for API to serve the file + const servePath = `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}` + + logger.info(`Generated presigned URL for ${fileName} (${uniqueKey})`) + + return NextResponse.json({ + presignedUrl, + fileInfo: { + path: servePath, + key: uniqueKey, + name: fileName, + size: fileSize, + type: contentType + }, + directUploadSupported: true + }) + } catch (error) { + logger.error('Error generating presigned URL:', error) + return createErrorResponse(error instanceof Error ? error : new Error('Failed to generate presigned URL')) + } +} + +// Handle preflight requests +export async function OPTIONS() { + return createOptionsResponse() +} \ No newline at end of file diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx index 5f0a53f4c9..b2d627e7d7 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-upload.tsx @@ -140,35 +140,84 @@ export function FileUpload({ const uploadedFiles: UploadedFile[] = [] const uploadErrors: string[] = [] + // Try to get pre-signed URLs first for direct upload + let useDirectUpload = false + // Upload each file separately for (const file of validFiles) { - // Create FormData for upload - const formData = new FormData() - formData.append('file', file) + try { + // First, try to get a pre-signed URL for direct upload + const presignedResponse = await fetch('/api/files/presigned', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileName: file.name, + contentType: file.type, + fileSize: file.size, + }), + }) - // Upload the file - const response = await fetch('/api/files/upload', { - method: 'POST', - body: formData, - }) + const presignedData = await presignedResponse.json() + + if (presignedResponse.ok && presignedData.directUploadSupported) { + // Use direct upload method + useDirectUpload = true + + // Upload directly to S3 using the pre-signed URL + const uploadResponse = await fetch(presignedData.presignedUrl, { + method: 'PUT', + headers: { + 'Content-Type': file.type, + }, + body: file, + }) - // Handle error response - if (!response.ok) { - const errorData = await response.json().catch(() => ({ error: response.statusText })) - const errorMessage = errorData.error || `Failed to upload file: ${response.status}` + if (!uploadResponse.ok) { + throw new Error(`Direct upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`) + } + + // Use the file info returned from the presigned URL endpoint + uploadedFiles.push(presignedData.fileInfo) + + } else { + // Fallback to traditional upload through API route + useDirectUpload = false + + // Create FormData for upload + const formData = new FormData() + formData.append('file', file) + + // Upload the file via server + const response = await fetch('/api/files/upload', { + method: 'POST', + body: formData, + }) + + // Handle error response + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })) + const errorMessage = errorData.error || `Failed to upload file: ${response.status}` + uploadErrors.push(`${file.name}: ${errorMessage}`) + continue + } + + // Process successful upload + const data = await response.json() + + uploadedFiles.push({ + name: file.name, + path: data.path, + size: file.size, + type: file.type, + }) + } + } catch (error) { + console.error(`Error uploading ${file.name}:`, error) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' uploadErrors.push(`${file.name}: ${errorMessage}`) - continue } - - // Process successful upload - const data = await response.json() - - uploadedFiles.push({ - name: file.name, - path: data.path, - size: file.size, - type: file.type, - }) } // Clear progress interval @@ -181,16 +230,17 @@ export function FileUpload({ // Send consolidated notification about uploaded files if (uploadedFiles.length > 0) { + const uploadMethod = useDirectUpload ? 'direct' : 'server' if (uploadedFiles.length === 1) { addNotification( 'console', - `${uploadedFiles[0].name} was uploaded successfully`, + `${uploadedFiles[0].name} was uploaded successfully (${uploadMethod} upload)`, activeWorkflowId ) } else { addNotification( 'console', - `Uploaded ${uploadedFiles.length} files successfully: ${uploadedFiles.map((f) => f.name).join(', ')}`, + `Uploaded ${uploadedFiles.length} files successfully: ${uploadedFiles.map((f) => f.name).join(', ')} (${uploadMethod} upload)`, activeWorkflowId ) } diff --git a/sim/blocks/blocks/file.ts b/sim/blocks/blocks/file.ts index d077ed5a88..0d13a79da5 100644 --- a/sim/blocks/blocks/file.ts +++ b/sim/blocks/blocks/file.ts @@ -40,7 +40,7 @@ const fileUploadBlock: SubBlockConfig = { layout: 'full' as SubBlockLayout, acceptedTypes: '.pdf,.csv,.docx', multiple: true, - maxSize: 100, + maxSize: 100, // 100MB max via direct upload } export const FileBlock: BlockConfig = { diff --git a/sim/blocks/blocks/mistral_parse.ts b/sim/blocks/blocks/mistral_parse.ts index 2993edb81a..00398863f6 100644 --- a/sim/blocks/blocks/mistral_parse.ts +++ b/sim/blocks/blocks/mistral_parse.ts @@ -29,7 +29,7 @@ const fileUploadBlock: SubBlockConfig = { field: 'inputMethod', value: 'upload', }, - maxSize: 50, + maxSize: 50, // 50MB max via direct upload } export const MistralParseBlock: BlockConfig = {