mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(file-upload): fixed client uploads to s3 by adding presigned url
This commit is contained in:
@@ -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()
|
||||
}
|
||||
+75
-25
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<FileParserOutput> = {
|
||||
|
||||
@@ -29,7 +29,7 @@ const fileUploadBlock: SubBlockConfig = {
|
||||
field: 'inputMethod',
|
||||
value: 'upload',
|
||||
},
|
||||
maxSize: 50,
|
||||
maxSize: 50, // 50MB max via direct upload
|
||||
}
|
||||
|
||||
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
||||
|
||||
Reference in New Issue
Block a user