fix(file-upload): fixed client uploads to s3 by adding presigned url

This commit is contained in:
Waleed Latif
2025-04-25 11:42:52 -07:00
parent 10bbc72e32
commit a9dfbba5b4
4 changed files with 156 additions and 27 deletions
+79
View File
@@ -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()
}
@@ -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
)
}
+1 -1
View File
@@ -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> = {
+1 -1
View File
@@ -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> = {