mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* fix(logs): execution files should always use our internal route * correct degree of access control * fix tests * fix tag defs flag * fix type check * fix mcp tools * make webhooks consistent * fix ollama and vllm visibility * remove dup test
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
import { readFile } from 'fs/promises'
|
|
import { createLogger } from '@sim/logger'
|
|
import type { NextRequest } from 'next/server'
|
|
import { NextResponse } from 'next/server'
|
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
|
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
|
|
import type { StorageContext } from '@/lib/uploads/config'
|
|
import { downloadFile } from '@/lib/uploads/core/storage-service'
|
|
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
|
import { verifyFileAccess } from '@/app/api/files/authorization'
|
|
import {
|
|
createErrorResponse,
|
|
createFileResponse,
|
|
FileNotFoundError,
|
|
findLocalFile,
|
|
getContentType,
|
|
} from '@/app/api/files/utils'
|
|
|
|
const logger = createLogger('FilesServeAPI')
|
|
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ path: string[] }> }
|
|
) {
|
|
try {
|
|
const { path } = await params
|
|
|
|
if (!path || path.length === 0) {
|
|
throw new FileNotFoundError('No file path provided')
|
|
}
|
|
|
|
logger.info('File serve request:', { path })
|
|
|
|
const fullPath = path.join('/')
|
|
const isS3Path = path[0] === 's3'
|
|
const isBlobPath = path[0] === 'blob'
|
|
const isCloudPath = isS3Path || isBlobPath
|
|
const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath
|
|
|
|
const contextParam = request.nextUrl.searchParams.get('context')
|
|
|
|
const context = contextParam || (isCloudPath ? inferContextFromKey(cloudKey) : undefined)
|
|
|
|
if (context === 'profile-pictures' || context === 'og-images') {
|
|
logger.info(`Serving public ${context}:`, { cloudKey })
|
|
if (isUsingCloudStorage() || isCloudPath) {
|
|
return await handleCloudProxyPublic(cloudKey, context)
|
|
}
|
|
return await handleLocalFilePublic(fullPath)
|
|
}
|
|
|
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
|
|
|
if (!authResult.success || !authResult.userId) {
|
|
logger.warn('Unauthorized file access attempt', {
|
|
path,
|
|
error: authResult.error || 'Missing userId',
|
|
})
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const userId = authResult.userId
|
|
|
|
if (isUsingCloudStorage()) {
|
|
return await handleCloudProxy(cloudKey, userId, contextParam)
|
|
}
|
|
|
|
return await handleLocalFile(cloudKey, userId)
|
|
} catch (error) {
|
|
logger.error('Error serving file:', error)
|
|
|
|
if (error instanceof FileNotFoundError) {
|
|
return createErrorResponse(error)
|
|
}
|
|
|
|
return createErrorResponse(error instanceof Error ? error : new Error('Failed to serve file'))
|
|
}
|
|
}
|
|
|
|
async function handleLocalFile(filename: string, userId: string): Promise<NextResponse> {
|
|
try {
|
|
const contextParam: StorageContext | undefined = inferContextFromKey(filename) as
|
|
| StorageContext
|
|
| undefined
|
|
|
|
const hasAccess = await verifyFileAccess(
|
|
filename,
|
|
userId,
|
|
undefined, // customConfig
|
|
contextParam, // context
|
|
true // isLocal
|
|
)
|
|
|
|
if (!hasAccess) {
|
|
logger.warn('Unauthorized local file access attempt', { userId, filename })
|
|
throw new FileNotFoundError(`File not found: ${filename}`)
|
|
}
|
|
|
|
const filePath = findLocalFile(filename)
|
|
|
|
if (!filePath) {
|
|
throw new FileNotFoundError(`File not found: ${filename}`)
|
|
}
|
|
|
|
const fileBuffer = await readFile(filePath)
|
|
const contentType = getContentType(filename)
|
|
|
|
logger.info('Local file served', { userId, filename, size: fileBuffer.length })
|
|
|
|
return createFileResponse({
|
|
buffer: fileBuffer,
|
|
contentType,
|
|
filename,
|
|
})
|
|
} catch (error) {
|
|
logger.error('Error reading local file:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function handleCloudProxy(
|
|
cloudKey: string,
|
|
userId: string,
|
|
contextParam?: string | null
|
|
): Promise<NextResponse> {
|
|
try {
|
|
let context: StorageContext
|
|
|
|
if (contextParam) {
|
|
context = contextParam as StorageContext
|
|
logger.info(`Using explicit context: ${context} for key: ${cloudKey}`)
|
|
} else {
|
|
context = inferContextFromKey(cloudKey)
|
|
logger.info(`Inferred context: ${context} from key pattern: ${cloudKey}`)
|
|
}
|
|
|
|
const hasAccess = await verifyFileAccess(
|
|
cloudKey,
|
|
userId,
|
|
undefined, // customConfig
|
|
context, // context
|
|
false // isLocal
|
|
)
|
|
|
|
if (!hasAccess) {
|
|
logger.warn('Unauthorized cloud file access attempt', { userId, key: cloudKey, context })
|
|
throw new FileNotFoundError(`File not found: ${cloudKey}`)
|
|
}
|
|
|
|
let fileBuffer: Buffer
|
|
|
|
if (context === 'copilot') {
|
|
fileBuffer = await CopilotFiles.downloadCopilotFile(cloudKey)
|
|
} else {
|
|
fileBuffer = await downloadFile({
|
|
key: cloudKey,
|
|
context,
|
|
})
|
|
}
|
|
|
|
const originalFilename = cloudKey.split('/').pop() || 'download'
|
|
const contentType = getContentType(originalFilename)
|
|
|
|
logger.info('Cloud file served', {
|
|
userId,
|
|
key: cloudKey,
|
|
size: fileBuffer.length,
|
|
context,
|
|
})
|
|
|
|
return createFileResponse({
|
|
buffer: fileBuffer,
|
|
contentType,
|
|
filename: originalFilename,
|
|
})
|
|
} catch (error) {
|
|
logger.error('Error downloading from cloud storage:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function handleCloudProxyPublic(
|
|
cloudKey: string,
|
|
context: StorageContext
|
|
): Promise<NextResponse> {
|
|
try {
|
|
let fileBuffer: Buffer
|
|
|
|
if (context === 'copilot') {
|
|
fileBuffer = await CopilotFiles.downloadCopilotFile(cloudKey)
|
|
} else {
|
|
fileBuffer = await downloadFile({
|
|
key: cloudKey,
|
|
context,
|
|
})
|
|
}
|
|
|
|
const originalFilename = cloudKey.split('/').pop() || 'download'
|
|
const contentType = getContentType(originalFilename)
|
|
|
|
logger.info('Public cloud file served', {
|
|
key: cloudKey,
|
|
size: fileBuffer.length,
|
|
context,
|
|
})
|
|
|
|
return createFileResponse({
|
|
buffer: fileBuffer,
|
|
contentType,
|
|
filename: originalFilename,
|
|
})
|
|
} catch (error) {
|
|
logger.error('Error serving public cloud file:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function handleLocalFilePublic(filename: string): Promise<NextResponse> {
|
|
try {
|
|
const filePath = findLocalFile(filename)
|
|
|
|
if (!filePath) {
|
|
throw new FileNotFoundError(`File not found: ${filename}`)
|
|
}
|
|
|
|
const fileBuffer = await readFile(filePath)
|
|
const contentType = getContentType(filename)
|
|
|
|
logger.info('Public local file served', { filename, size: fileBuffer.length })
|
|
|
|
return createFileResponse({
|
|
buffer: fileBuffer,
|
|
contentType,
|
|
filename,
|
|
})
|
|
} catch (error) {
|
|
logger.error('Error reading public local file:', error)
|
|
throw error
|
|
}
|
|
}
|