chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { sanitizeFileName } from '@/executor/constants'
|
||||
import '@/lib/uploads/core/setup.server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
|
||||
import {
|
||||
uploadFilesFormFieldsSchema,
|
||||
uploadFilesFormFilesSchema,
|
||||
} from '@/lib/api/contracts/storage-transfer'
|
||||
import { getValidationErrorMessage } from '@/lib/api/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import {
|
||||
assertKnownSizeWithinLimit,
|
||||
isPayloadSizeLimitError,
|
||||
MAX_MULTIPART_OVERHEAD_BYTES,
|
||||
readFileToBufferWithLimit,
|
||||
readFormDataWithLimit,
|
||||
} from '@/lib/core/utils/stream-limits'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { captureServerEvent } from '@/lib/posthog/server'
|
||||
import type { StorageContext } from '@/lib/uploads/config'
|
||||
import { generateKnowledgeBaseFileKey } from '@/lib/uploads/contexts/knowledge-base/knowledge-base-file-manager'
|
||||
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
|
||||
import { MAX_WORKSPACE_FORMDATA_FILE_SIZE } from '@/lib/uploads/shared/types'
|
||||
import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
|
||||
import {
|
||||
SUPPORTED_ATTACHMENT_EXTENSIONS,
|
||||
SUPPORTED_IMAGE_EXTENSIONS,
|
||||
validateFileType,
|
||||
} from '@/lib/uploads/utils/validation'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { createErrorResponse, InvalidRequestError } from '@/app/api/files/utils'
|
||||
|
||||
const ALLOWED_EXTENSIONS = new Set<string>(SUPPORTED_ATTACHMENT_EXTENSIONS)
|
||||
|
||||
function validateFileExtension(filename: string): boolean {
|
||||
const extension = filename.split('.').pop()?.toLowerCase()
|
||||
if (!extension) return false
|
||||
return ALLOWED_EXTENSIONS.has(extension)
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('FilesUploadAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const formData = await readFormDataWithLimit(request, {
|
||||
maxBytes: MAX_WORKSPACE_FORMDATA_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES,
|
||||
label: 'multipart upload body',
|
||||
})
|
||||
|
||||
const rawFiles = formData.getAll('file')
|
||||
const filesResult = uploadFilesFormFilesSchema.safeParse(rawFiles)
|
||||
if (!filesResult.success) {
|
||||
throw new InvalidRequestError('No files provided')
|
||||
}
|
||||
const files = filesResult.data
|
||||
const totalFileSize = files.reduce((total, file) => total + file.size, 0)
|
||||
assertKnownSizeWithinLimit(totalFileSize, MAX_WORKSPACE_FORMDATA_FILE_SIZE, 'uploaded files')
|
||||
|
||||
const formFieldsResult = uploadFilesFormFieldsSchema.safeParse({
|
||||
workflowId: formData.get('workflowId'),
|
||||
executionId: formData.get('executionId'),
|
||||
workspaceId: formData.get('workspaceId'),
|
||||
context: formData.get('context'),
|
||||
})
|
||||
if (!formFieldsResult.success) {
|
||||
throw new InvalidRequestError(
|
||||
getValidationErrorMessage(formFieldsResult.error, 'Invalid upload form data')
|
||||
)
|
||||
}
|
||||
const formFields = formFieldsResult.data
|
||||
const { workflowId, executionId, workspaceId, context: contextParam } = formFields
|
||||
|
||||
// Context must be explicitly provided
|
||||
if (!contextParam) {
|
||||
throw new InvalidRequestError(
|
||||
'Upload requires explicit context parameter (knowledge-base, workspace, execution, copilot, chat, profile-pictures, or workspace-logos)'
|
||||
)
|
||||
}
|
||||
|
||||
const context = contextParam as StorageContext
|
||||
|
||||
const storageService = await import('@/lib/uploads/core/storage-service')
|
||||
const usingCloudStorage = storageService.hasCloudStorage()
|
||||
logger.info(`Using storage mode: ${usingCloudStorage ? 'Cloud' : 'Local'} for file upload`)
|
||||
|
||||
// Execution context requires a workspace write/admin permission check. Resolve it once per
|
||||
// request (not per file) since workspaceId is invariant across all files in the upload.
|
||||
let executionUploadContext:
|
||||
| { workspaceId: string; workflowId: string; executionId: string }
|
||||
| undefined
|
||||
if (context === 'execution') {
|
||||
if (!workflowId || !executionId || !workspaceId) {
|
||||
throw new InvalidRequestError(
|
||||
'Execution context requires workflowId, executionId, and workspaceId parameters'
|
||||
)
|
||||
}
|
||||
|
||||
const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
|
||||
if (permission !== 'write' && permission !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Write or Admin access required for execution uploads' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
executionUploadContext = { workspaceId, workflowId, executionId }
|
||||
}
|
||||
|
||||
// Mothership context requires the same workspace write/admin permission check, plus a
|
||||
// storage quota check. Resolve both once per request (not per file) since workspaceId is
|
||||
// invariant across all files in the upload and quota must account for the full batch size,
|
||||
// not just one file.
|
||||
let mothershipWorkspaceId: string | undefined
|
||||
if (context === 'mothership') {
|
||||
if (!workspaceId) {
|
||||
throw new InvalidRequestError('Mothership context requires workspaceId parameter')
|
||||
}
|
||||
|
||||
const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
|
||||
if (permission !== 'write' && permission !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Write or Admin access required for mothership uploads' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const { checkStorageQuota } = await import('@/lib/billing/storage')
|
||||
const quotaCheck = await checkStorageQuota(session.user.id, totalFileSize)
|
||||
if (!quotaCheck.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: quotaCheck.error || 'Storage limit exceeded' },
|
||||
{ status: 413 }
|
||||
)
|
||||
}
|
||||
|
||||
mothershipWorkspaceId = workspaceId
|
||||
}
|
||||
|
||||
const uploadResults = []
|
||||
|
||||
for (const file of files) {
|
||||
const originalName = file.name || 'untitled.md'
|
||||
|
||||
if (!validateFileExtension(originalName)) {
|
||||
const extension = originalName.split('.').pop()?.toLowerCase() || 'unknown'
|
||||
throw new InvalidRequestError(
|
||||
`File type '${extension}' is not allowed. Allowed types: ${Array.from(ALLOWED_EXTENSIONS).join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
const buffer = await readFileToBufferWithLimit(file, {
|
||||
maxBytes: MAX_WORKSPACE_FORMDATA_FILE_SIZE,
|
||||
label: 'uploaded file',
|
||||
})
|
||||
|
||||
// Handle execution context
|
||||
if (context === 'execution' && executionUploadContext) {
|
||||
const { uploadExecutionFile } = await import('@/lib/uploads/contexts/execution')
|
||||
const userFile = await uploadExecutionFile(
|
||||
executionUploadContext,
|
||||
buffer,
|
||||
originalName,
|
||||
file.type,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
uploadResults.push(userFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle knowledge-base context
|
||||
if (context === 'knowledge-base') {
|
||||
// Validate file type for knowledge base
|
||||
const validationError = validateFileType(originalName, file.type)
|
||||
if (validationError) {
|
||||
throw new InvalidRequestError(validationError.message)
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new InvalidRequestError('workspaceId is required for knowledge-base uploads')
|
||||
}
|
||||
|
||||
const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
|
||||
if (permission !== 'write' && permission !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Write or Admin access required for knowledge-base uploads' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`Uploading knowledge-base file: ${originalName}`)
|
||||
|
||||
const storageKey = generateKnowledgeBaseFileKey(originalName)
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
originalName: originalName,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
purpose: 'knowledge-base',
|
||||
userId: session.user.id,
|
||||
workspaceId,
|
||||
}
|
||||
|
||||
const fileInfo = await storageService.uploadFile({
|
||||
file: buffer,
|
||||
fileName: storageKey,
|
||||
contentType: file.type,
|
||||
context: 'knowledge-base',
|
||||
preserveKey: true,
|
||||
customKey: storageKey,
|
||||
metadata,
|
||||
})
|
||||
|
||||
const finalPath = usingCloudStorage
|
||||
? `${fileInfo.path}?context=knowledge-base`
|
||||
: fileInfo.path
|
||||
|
||||
const uploadResult = {
|
||||
fileName: originalName,
|
||||
presignedUrl: '', // Not used for server-side uploads
|
||||
fileInfo: {
|
||||
path: finalPath,
|
||||
key: fileInfo.key,
|
||||
name: originalName,
|
||||
size: buffer.length,
|
||||
type: file.type,
|
||||
},
|
||||
directUploadSupported: false,
|
||||
}
|
||||
|
||||
logger.info(`Successfully uploaded knowledge-base file: ${fileInfo.key}`)
|
||||
uploadResults.push(uploadResult)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle workspace context
|
||||
if (context === 'workspace') {
|
||||
if (!workspaceId) {
|
||||
throw new InvalidRequestError('Workspace context requires workspaceId parameter')
|
||||
}
|
||||
const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
|
||||
if (permission !== 'admin' && permission !== 'write') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Write or Admin access required for workspace uploads' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const { uploadWorkspaceFile } = await import('@/lib/uploads/contexts/workspace')
|
||||
const userFile = await uploadWorkspaceFile(
|
||||
workspaceId,
|
||||
session.user.id,
|
||||
buffer,
|
||||
originalName,
|
||||
file.type || 'application/octet-stream'
|
||||
)
|
||||
|
||||
uploadResults.push(userFile)
|
||||
continue
|
||||
} catch (workspaceError) {
|
||||
const errorMessage = getErrorMessage(workspaceError, 'Upload failed')
|
||||
const isDuplicate = errorMessage.includes('already exists')
|
||||
const isStorageLimitError =
|
||||
errorMessage.includes('Storage limit exceeded') ||
|
||||
errorMessage.includes('storage limit')
|
||||
|
||||
logger.warn(`Workspace file upload failed: ${errorMessage}`)
|
||||
|
||||
let statusCode = 500
|
||||
if (isDuplicate) statusCode = 409
|
||||
else if (isStorageLimitError) statusCode = 413
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
isDuplicate,
|
||||
},
|
||||
{ status: statusCode }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mothership context (chat-scoped uploads to workspace S3)
|
||||
if (context === 'mothership' && mothershipWorkspaceId) {
|
||||
logger.info(`Uploading mothership file: ${originalName}`)
|
||||
|
||||
const storageKey = generateWorkspaceFileKey(mothershipWorkspaceId, originalName)
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
originalName: originalName,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
purpose: 'mothership',
|
||||
userId: session.user.id,
|
||||
workspaceId: mothershipWorkspaceId,
|
||||
}
|
||||
|
||||
const fileInfo = await storageService.uploadFile({
|
||||
file: buffer,
|
||||
fileName: storageKey,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
context: 'mothership',
|
||||
preserveKey: true,
|
||||
customKey: storageKey,
|
||||
metadata,
|
||||
})
|
||||
|
||||
const finalPath = usingCloudStorage ? `${fileInfo.path}?context=mothership` : fileInfo.path
|
||||
|
||||
uploadResults.push({
|
||||
fileName: originalName,
|
||||
presignedUrl: '',
|
||||
fileInfo: {
|
||||
path: finalPath,
|
||||
key: fileInfo.key,
|
||||
name: originalName,
|
||||
size: buffer.length,
|
||||
type: file.type || 'application/octet-stream',
|
||||
},
|
||||
directUploadSupported: false,
|
||||
})
|
||||
|
||||
logger.info(`Successfully uploaded mothership file: ${fileInfo.key}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
context === 'copilot' ||
|
||||
context === 'chat' ||
|
||||
context === 'profile-pictures' ||
|
||||
context === 'workspace-logos'
|
||||
) {
|
||||
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') {
|
||||
if (!workspaceId) {
|
||||
throw new InvalidRequestError('workspace-logos context requires workspaceId parameter')
|
||||
}
|
||||
const permission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workspaceId
|
||||
)
|
||||
if (permission !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Admin access required for workspace logo uploads' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (context === 'chat' && workspaceId) {
|
||||
const permission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workspaceId
|
||||
)
|
||||
if (permission === null) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Insufficient permissions for workspace' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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}`
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
originalName: originalName,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
purpose: context,
|
||||
userId: session.user.id,
|
||||
}
|
||||
|
||||
if (workspaceId && context === 'chat') {
|
||||
metadata.workspaceId = workspaceId
|
||||
}
|
||||
|
||||
const fileInfo = await storageService.uploadFile({
|
||||
file: buffer,
|
||||
fileName: storageKey,
|
||||
contentType: resolvedContentType,
|
||||
context,
|
||||
preserveKey: true,
|
||||
customKey: storageKey,
|
||||
metadata,
|
||||
})
|
||||
|
||||
const finalPath = usingCloudStorage ? `${fileInfo.path}?context=${context}` : fileInfo.path
|
||||
|
||||
const uploadResult = {
|
||||
fileName: originalName,
|
||||
presignedUrl: '', // Not used for server-side uploads
|
||||
fileInfo: {
|
||||
path: finalPath,
|
||||
key: fileInfo.key,
|
||||
name: originalName,
|
||||
size: buffer.length,
|
||||
type: resolvedContentType,
|
||||
},
|
||||
directUploadSupported: false,
|
||||
}
|
||||
|
||||
logger.info(`Successfully uploaded ${context} file: ${fileInfo.key}`)
|
||||
|
||||
if (context === 'workspace-logos' && workspaceId) {
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.FILE_UPLOADED,
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: workspaceId,
|
||||
description: `Uploaded workspace logo "${originalName}"`,
|
||||
metadata: {
|
||||
fileName: originalName,
|
||||
fileKey: fileInfo.key,
|
||||
fileSize: buffer.length,
|
||||
fileType: resolvedContentType,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
captureServerEvent(session.user.id, 'workspace_logo_uploaded', {
|
||||
workspace_id: workspaceId,
|
||||
file_name: originalName,
|
||||
file_size: buffer.length,
|
||||
})
|
||||
}
|
||||
|
||||
uploadResults.push(uploadResult)
|
||||
continue
|
||||
}
|
||||
|
||||
// Unknown context
|
||||
throw new InvalidRequestError(
|
||||
`Unsupported context: ${context}. Use knowledge-base, workspace, execution, copilot, chat, profile-pictures, or workspace-logos`
|
||||
)
|
||||
}
|
||||
|
||||
if (uploadResults.length === 1) {
|
||||
return NextResponse.json(uploadResults[0])
|
||||
}
|
||||
return NextResponse.json({ files: uploadResults })
|
||||
} catch (error) {
|
||||
logger.error('Error in file upload:', error)
|
||||
if (isPayloadSizeLimitError(error)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'PayloadSizeLimitError',
|
||||
message: `File exceeds the server upload limit of ${Math.round(error.maxBytes / (1024 * 1024))}MB. Use direct upload for larger workspace files.`,
|
||||
},
|
||||
{ status: 413 }
|
||||
)
|
||||
}
|
||||
return createErrorResponse(error instanceof Error ? error : new Error('File upload failed'))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user