import { Buffer, isUtf8 } from 'buffer' import { createHash } from 'crypto' import fsPromises from 'fs/promises' import path from 'path' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import binaryExtensionsList from 'binary-extensions' import { type NextRequest, NextResponse } from 'next/server' import { fileParseContract } from '@/lib/api/contracts/storage-transfer' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { assertKnownSizeWithinLimit, isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { isSupportedFileType, parseFile } from '@/lib/file-parsers' import { isUsingCloudStorage, StorageService } from '@/lib/uploads' import { uploadExecutionFile } from '@/lib/uploads/contexts/execution' import { ExternalUrlValidationError, fetchExternalUrlToWorkspace, } from '@/lib/uploads/contexts/workspace' import { UPLOAD_DIR_SERVER } from '@/lib/uploads/core/setup.server' import { getFileMetadataByKey } from '@/lib/uploads/server/metadata' import { extractCleanFilename, extractStorageKey, extractWorkspaceIdFromExecutionKey, getMimeTypeFromExtension, getViewerUrl, inferContextFromKey, isInternalFileUrl, } from '@/lib/uploads/utils/file-utils' import { verifyFileAccess } from '@/app/api/files/authorization' import type { UserFile } from '@/executor/types' import '@/lib/uploads/core/setup.server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' export const dynamic = 'force-dynamic' const logger = createLogger('FilesParseAPI') const MAX_DOWNLOAD_SIZE_BYTES = 100 * 1024 * 1024 // 100 MB const DOWNLOAD_TIMEOUT_MS = 30000 // 30 seconds const MAX_FILE_REFERENCE_LENGTH = 4096 const MAX_MULTI_FILE_PARSE_OUTPUT_BYTES = 5 * 1024 * 1024 const BINARY_EXTENSIONS = new Set(binaryExtensionsList) function isLikelyTextBuffer(fileBuffer: Buffer): boolean { return isUtf8(fileBuffer) && !fileBuffer.includes(0) } interface ExecutionContext { workspaceId: string workflowId: string executionId: string } interface ParseResult { success: boolean content?: string error?: string filePath: string originalName?: string // Original filename from database (for workspace files) viewerUrl?: string | null // Viewer URL for the file if available userFile?: UserFile // UserFile object for the raw file metadata?: { fileType: string size: number hash: string processingTime: number } } function getContentBytes(content: unknown): number { return typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : 0 } /** * Main API route handler */ export const POST = withRouteHandler(async (request: NextRequest) => { const startTime = Date.now() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: true }) if (!authResult.success) { logger.warn('Unauthorized file parse request', { error: authResult.error || 'Authentication failed', }) return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) } if (!authResult.userId) { logger.warn('File parse request missing userId', { authType: authResult.authType, }) return NextResponse.json({ success: false, error: 'User context required' }, { status: 401 }) } const userId = authResult.userId const parsed = await parseRequest( fileParseContract, request, {}, { validationErrorResponse: (error) => { const message = getValidationErrorMessage(error, 'Invalid request data') return NextResponse.json( { success: false, error: message, filePath: '', }, { status: message.includes('At most 10 files') ? 413 : 400 } ) }, } ) if (!parsed.success) return parsed.response const { filePath, fileType, headers, workspaceId, workflowId, executionId } = parsed.data.body if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) { return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 }) } // Build execution context if all required fields are present const executionContext: ExecutionContext | undefined = workspaceId && workflowId && executionId ? { workspaceId, workflowId, executionId } : undefined logger.info('File parse request received:', { filePath, fileType, workspaceId, userId, hasExecutionContext: !!executionContext, hasHeaders: Boolean(headers && Object.keys(headers).length > 0), }) if (Array.isArray(filePath)) { const results = [] let totalOutputBytes = 0 for (const singlePath of filePath) { if (!singlePath || (typeof singlePath === 'string' && singlePath.trim() === '')) { results.push({ success: false, error: 'Empty file path in array', filePath: singlePath || '', }) continue } const remainingOutputBytes = MAX_MULTI_FILE_PARSE_OUTPUT_BYTES - totalOutputBytes if (remainingOutputBytes <= 0) { return parsedOutputTooLargeResponse(results) } const result = await parseFileSingle( singlePath, fileType, workspaceId, userId, executionContext, headers, request.signal, MAX_DOWNLOAD_SIZE_BYTES, remainingOutputBytes ) if (result.metadata) { result.metadata.processingTime = Date.now() - startTime } if (result.success) { totalOutputBytes += getContentBytes(result.content) if (totalOutputBytes > MAX_MULTI_FILE_PARSE_OUTPUT_BYTES) { return parsedOutputTooLargeResponse(results) } const displayName = result.originalName || extractCleanFilename(result.filePath) || 'unknown' results.push({ success: true, output: { content: result.content, name: displayName, fileType: result.metadata?.fileType || 'application/octet-stream', size: result.metadata?.size || 0, binary: false, file: result.userFile, }, filePath: result.filePath, viewerUrl: result.viewerUrl, }) continue } if (result.error?.startsWith('Parsed file output is too large')) { return parsedOutputTooLargeResponse(results) } results.push(result) } return NextResponse.json({ success: true, results, }) } const result = await parseFileSingle( filePath, fileType, workspaceId, userId, executionContext, headers, request.signal ) if (result.metadata) { result.metadata.processingTime = Date.now() - startTime } if (result.success) { const displayName = result.originalName || extractCleanFilename(result.filePath) || 'unknown' return NextResponse.json({ success: true, output: { content: result.content, name: displayName, fileType: result.metadata?.fileType || 'application/octet-stream', size: result.metadata?.size || 0, binary: false, file: result.userFile, }, filePath: result.filePath, viewerUrl: result.viewerUrl, }) } return NextResponse.json(result) } catch (error) { logger.error('Error in file parse API:', error) return NextResponse.json( { success: false, error: getErrorMessage(error, 'Unknown error occurred'), filePath: '', }, { status: 500 } ) } }) /** * Parse a single file and return its content */ async function parseFileSingle( filePath: string, fileType: string, workspaceId: string, userId: string, executionContext?: ExecutionContext, headers?: Record, signal?: AbortSignal, maxDownloadBytes = MAX_DOWNLOAD_SIZE_BYTES, maxParsedOutputBytes?: number ): Promise { logger.info('Parsing file:', filePath) if (!filePath || filePath.trim() === '') { return { success: false, error: 'Empty file path provided', filePath: filePath || '', } } const referenceValidation = validateFileReferenceShape(filePath) if (!referenceValidation.isValid) { return { success: false, error: referenceValidation.error || 'Invalid file reference', filePath, } } const pathValidation = validateFilePath(filePath) if (!pathValidation.isValid) { return { success: false, error: pathValidation.error || 'Invalid path', filePath, } } if (isInternalFileUrl(filePath)) { return handleCloudFile( filePath, fileType, userId, executionContext, maxDownloadBytes, maxParsedOutputBytes ) } if (filePath.startsWith('http://') || filePath.startsWith('https://')) { return handleExternalUrl( filePath, fileType, workspaceId, userId, executionContext, headers, signal, maxDownloadBytes, maxParsedOutputBytes ) } if (isUsingCloudStorage()) { return handleCloudFile( filePath, fileType, userId, executionContext, maxDownloadBytes, maxParsedOutputBytes ) } return handleLocalFile( filePath, fileType, userId, executionContext, maxDownloadBytes, maxParsedOutputBytes ) } function validateFileReferenceShape(filePath: string): { isValid: boolean; error?: string } { const trimmed = filePath.trim() if ( trimmed.startsWith('http://') || trimmed.startsWith('https://') || isInternalFileUrl(trimmed) ) { return { isValid: true } } if (trimmed.startsWith('data:')) { return { isValid: false, error: 'File input must be a URL or uploaded file reference, not inline file content', } } if (filePath.length > MAX_FILE_REFERENCE_LENGTH) { return { isValid: false, error: 'File reference is too long; provide a file URL or upload the file instead', } } if (/[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(filePath)) { return { isValid: false, error: 'File reference contains binary content; provide a file URL or upload the file instead', } } const newlineCount = filePath.match(/\r\n|\r|\n/g)?.length ?? 0 if (newlineCount > 2) { return { isValid: false, error: 'File reference looks like inline file content; provide a file URL or upload the file instead', } } return { isValid: true } } function parsedOutputTooLargeResponse(results?: unknown[]): NextResponse { const hasPartialResults = Boolean(results && results.length > 0) return NextResponse.json( { success: hasPartialResults, error: `Parsed file output is too large to return safely. Maximum combined parsed output is ${prettySize( MAX_MULTI_FILE_PARSE_OUTPUT_BYTES )}.`, ...(results && results.length > 0 ? { results } : {}), }, { status: hasPartialResults ? 200 : 413 } ) } function getParsedOutputTooLargeMessage(maxBytes: number): string { return `Parsed file output is too large to return safely. Maximum parsed output is ${prettySize( maxBytes )}.` } function assertParsedContentWithinLimit(content: string, maxBytes?: number): string { if (maxBytes !== undefined) { assertKnownSizeWithinLimit(Buffer.byteLength(content, 'utf8'), maxBytes, 'parsed file output') } return content } /** * Validate file path for security - prevents null byte injection and path traversal attacks. * * External URLs (`http`/`https`) are fetched over HTTP — with SSRF protection applied * downstream in `fetchExternalUrlToWorkspace` (DNS resolution + private/reserved IP blocking) * — and are never resolved against the filesystem, so `..`/`~` are legal URL content and must * not be rejected. Providers such as Slack routinely emit slugs containing a literal `...`. * * Internal file URLs (`/api/files/serve/...`) ARE resolved to storage keys and filesystem * paths via `extractStorageKey`, so they keep full traversal protection. The external * short-circuit explicitly excludes them: `parseFileSingle` routes anything matching * `isInternalFileUrl` to `handleCloudFile` (even an absolute `https://host/api/files/serve/...`), * so such inputs must stay subject to the `..`/`~` checks rather than being waved through as * external URLs. Only the leading-`/` "outside allowed directory" check is relaxed for them, * since that prefix is expected. */ function validateFilePath(filePath: string): { isValid: boolean; error?: string } { if (filePath.includes('\0')) { return { isValid: false, error: 'Invalid path: null byte detected' } } if ( (filePath.startsWith('http://') || filePath.startsWith('https://')) && !isInternalFileUrl(filePath) ) { return { isValid: true } } if (filePath.includes('..')) { return { isValid: false, error: 'Access denied: path traversal detected' } } if (filePath.includes('~')) { return { isValid: false, error: 'Invalid path: tilde character not allowed' } } if (filePath.startsWith('/') && !isInternalFileUrl(filePath)) { return { isValid: false, error: 'Path outside allowed directory' } } if (/^[A-Za-z]:\\/.test(filePath)) { return { isValid: false, error: 'Path outside allowed directory' } } return { isValid: true } } /** * Handle external URL. * * Always fetches the URL fresh — there is no filename-based dedup. Distinct URLs * commonly share a path tail (e.g. every Slack clipboard paste is `image.png`), * so keying a cache by filename returns stale bytes. `fetchExternalUrlToWorkspace` * delegates to `uploadWorkspaceFile`, which suffix-disambiguates collisions on save. * * Workspace save is skipped when the URL already points at our execution-files * bucket (re-uploading our own bytes is wasteful and would generate `image (1).png` * style aliases for files we already own). */ async function handleExternalUrl( url: string, fileType: string, workspaceId: string, userId: string, executionContext?: ExecutionContext, headers?: Record, signal?: AbortSignal, maxDownloadBytes = MAX_DOWNLOAD_SIZE_BYTES, maxParsedOutputBytes?: number ): Promise { try { logger.info('Fetching external URL:', url) const { S3_EXECUTION_FILES_CONFIG, BLOB_EXECUTION_FILES_CONFIG, USE_S3_STORAGE, USE_BLOB_STORAGE, } = await import('@/lib/uploads/config') let isExecutionFile = false try { const parsedUrl = new URL(url) if (USE_S3_STORAGE && S3_EXECUTION_FILES_CONFIG.bucket) { const bucketInHost = parsedUrl.hostname.startsWith(S3_EXECUTION_FILES_CONFIG.bucket) const bucketInPath = parsedUrl.pathname.startsWith(`/${S3_EXECUTION_FILES_CONFIG.bucket}/`) isExecutionFile = bucketInHost || bucketInPath } else if (USE_BLOB_STORAGE && BLOB_EXECUTION_FILES_CONFIG.containerName) { isExecutionFile = url.includes(`/${BLOB_EXECUTION_FILES_CONFIG.containerName}/`) } } catch (error) { logger.warn('Failed to parse URL for execution file check:', error) isExecutionFile = false } const { filename, buffer, mimeType } = await fetchExternalUrlToWorkspace({ url, userId, workspaceId: workspaceId || undefined, saveToWorkspace: Boolean(workspaceId) && !isExecutionFile, headers, signal, maxDownloadBytes, timeoutMs: DOWNLOAD_TIMEOUT_MS, }) const extension = path.extname(filename).toLowerCase().substring(1) logger.info(`Downloaded file from URL: ${url}, size: ${buffer.length} bytes`) let userFile: UserFile | undefined if (executionContext) { try { userFile = await uploadExecutionFile(executionContext, buffer, filename, mimeType, userId) logger.info(`Stored file in execution storage: ${filename}`, { key: userFile.key }) } catch (uploadError) { logger.warn('Failed to store file in execution storage:', uploadError) } } let parseResult: ParseResult if (extension === 'pdf') { parseResult = await handlePdfBuffer(buffer, filename, fileType, url, maxParsedOutputBytes) } else if (extension === 'csv') { parseResult = await handleCsvBuffer(buffer, filename, fileType, url, maxParsedOutputBytes) } else if (isSupportedFileType(extension)) { parseResult = await handleGenericTextBuffer( buffer, filename, extension, fileType, url, maxParsedOutputBytes ) } else { parseResult = handleGenericBuffer(buffer, filename, extension, fileType, maxParsedOutputBytes) } // Attach userFile to the result if (userFile) { parseResult.userFile = userFile } return parseResult } catch (error) { logger.error(`Error handling external URL ${sanitizeUrlForLog(url)}:`, error) if (isPayloadSizeLimitError(error)) { logger.warn('Rejected oversized external file parse payload', { maxBytes: error.maxBytes, observedBytes: error.observedBytes, label: error.label, url: sanitizeUrlForLog(url), }) return { success: false, error: error.label === 'parsed file output' ? getParsedOutputTooLargeMessage(error.maxBytes) : `File is too large to parse safely. Maximum supported download size is ${prettySize( error.maxBytes )}.`, filePath: url, } } if (error instanceof ExternalUrlValidationError) { logger.warn(`Blocked external URL request: ${error.message}`) return { success: false, error: error.message, filePath: url, } } return { success: false, error: `Error fetching URL: ${(error as Error).message}`, filePath: url, } } } /** * Handle file stored in cloud storage * If executionContext is provided and file is not already from execution storage, * copies the file to execution storage and returns UserFile */ async function handleCloudFile( filePath: string, fileType: string, userId: string, executionContext?: ExecutionContext, maxDownloadBytes = MAX_DOWNLOAD_SIZE_BYTES, maxParsedOutputBytes?: number ): Promise { try { const cloudKey = extractStorageKey(filePath) logger.info('Extracted cloud key:', cloudKey) const context = inferContextFromKey(cloudKey) const hasAccess = await verifyFileAccess( cloudKey, userId, undefined, // customConfig context, // context false // isLocal ) if (!hasAccess) { logger.warn('Unauthorized cloud file parse attempt', { userId, key: cloudKey, context }) return { success: false, error: 'File not found', filePath, } } let originalFilename: string | undefined if (context === 'workspace') { try { const fileRecord = await getFileMetadataByKey(cloudKey, 'workspace') if (fileRecord) { originalFilename = fileRecord.originalName logger.debug(`Found original filename for workspace file: ${originalFilename}`) } } catch (dbError) { logger.debug(`Failed to lookup original filename for ${cloudKey}:`, dbError) } } const fileBuffer = await StorageService.downloadFile({ key: cloudKey, context, maxBytes: maxDownloadBytes, }) logger.info( `Downloaded file from ${context} storage: ${cloudKey}, size: ${fileBuffer.length} bytes` ) const filename = originalFilename || cloudKey.split('/').pop() || cloudKey const extension = path.extname(filename).toLowerCase().substring(1) const mimeType = getMimeTypeFromExtension(extension) const normalizedFilePath = `/api/files/serve/${encodeURIComponent(cloudKey)}?context=${context}` let workspaceIdFromKey: string | undefined if (context === 'execution') { workspaceIdFromKey = extractWorkspaceIdFromExecutionKey(cloudKey) || undefined } else if (context === 'workspace') { const segments = cloudKey.split('/') if (segments.length >= 2 && /^[a-f0-9-]{36}$/.test(segments[0])) { workspaceIdFromKey = segments[0] } } const viewerUrl = getViewerUrl(cloudKey, workspaceIdFromKey) // Store file in execution storage if executionContext is provided let userFile: UserFile | undefined if (executionContext) { // If file is already from execution context, create UserFile reference without re-uploading if (context === 'execution') { userFile = { id: `file_${Date.now()}_${generateShortId(7)}`, name: filename, url: normalizedFilePath, size: fileBuffer.length, type: mimeType, key: cloudKey, context: 'execution', } logger.info(`Created UserFile reference for existing execution file: ${filename}`) } else { // Copy from workspace/other storage to execution storage try { userFile = await uploadExecutionFile( executionContext, fileBuffer, filename, mimeType, userId ) logger.info(`Copied file to execution storage: ${filename}`, { key: userFile.key }) } catch (uploadError) { logger.warn(`Failed to copy file to execution storage:`, uploadError) } } } let parseResult: ParseResult if (extension === 'pdf') { parseResult = await handlePdfBuffer( fileBuffer, filename, fileType, normalizedFilePath, maxParsedOutputBytes ) } else if (extension === 'csv') { parseResult = await handleCsvBuffer( fileBuffer, filename, fileType, normalizedFilePath, maxParsedOutputBytes ) } else if (isSupportedFileType(extension)) { parseResult = await handleGenericTextBuffer( fileBuffer, filename, extension, fileType, normalizedFilePath, maxParsedOutputBytes ) } else { parseResult = handleGenericBuffer( fileBuffer, filename, extension, fileType, maxParsedOutputBytes ) parseResult.filePath = normalizedFilePath } if (originalFilename) { parseResult.originalName = originalFilename } parseResult.viewerUrl = viewerUrl // Attach userFile to the result if (userFile) { parseResult.userFile = userFile } return parseResult } catch (error) { logger.error(`Error handling cloud file ${filePath}:`, error) const errorMessage = (error as Error).message if (isPayloadSizeLimitError(error)) { logger.warn('Rejected oversized cloud file parse payload', { maxBytes: error.maxBytes, observedBytes: error.observedBytes, label: error.label, filePath, }) return { success: false, error: error.label === 'parsed file output' ? getParsedOutputTooLargeMessage(error.maxBytes) : `File is too large to parse safely. Maximum supported download size is ${prettySize( error.maxBytes )}.`, filePath, } } if (errorMessage.includes('Access denied') || errorMessage.includes('Forbidden')) { throw new Error(`Error accessing file from cloud storage: ${errorMessage}`) } return { success: false, error: `Error accessing file from cloud storage: ${errorMessage}`, filePath, } } } /** * Handle local file */ async function handleLocalFile( filePath: string, fileType: string, userId: string, executionContext?: ExecutionContext, maxDownloadBytes = MAX_DOWNLOAD_SIZE_BYTES, maxParsedOutputBytes?: number ): Promise { try { const storageKey = isInternalFileUrl(filePath) ? extractStorageKey(filePath) : filePath const filename = storageKey.split('/').pop() || storageKey const context = inferContextFromKey(storageKey) const hasAccess = await verifyFileAccess( storageKey, userId, undefined, // customConfig context, // context true // isLocal ) if (!hasAccess) { logger.warn('Unauthorized local file parse attempt', { userId, filename }) return { success: false, error: 'File not found', filePath, } } const fullPath = path.join(UPLOAD_DIR_SERVER, storageKey) logger.info('Processing local file:', fullPath) try { await fsPromises.access(fullPath) } catch { throw new Error(`File not found: ${filename}`) } const stats = await fsPromises.stat(fullPath) assertKnownSizeWithinLimit(stats.size, maxDownloadBytes, 'local file') const result = await parseFile(fullPath) const content = assertParsedContentWithinLimit(result.content, maxParsedOutputBytes) const fileBuffer = await fsPromises.readFile(fullPath) const hash = createHash('md5').update(fileBuffer).digest('hex') const extension = path.extname(filename).toLowerCase().substring(1) const mimeType = fileType || getMimeTypeFromExtension(extension) // Store file in execution storage if executionContext is provided let userFile: UserFile | undefined if (executionContext) { try { userFile = await uploadExecutionFile( executionContext, fileBuffer, filename, mimeType, userId ) logger.info(`Stored local file in execution storage: ${filename}`, { key: userFile.key }) } catch (uploadError) { logger.warn(`Failed to store local file in execution storage:`, uploadError) } } return { success: true, content, filePath, userFile, metadata: { fileType: mimeType, size: stats.size, hash, processingTime: 0, }, } } catch (error) { logger.error(`Error handling local file ${filePath}:`, error) if (isPayloadSizeLimitError(error)) { logger.warn('Rejected oversized local file parse payload', { maxBytes: error.maxBytes, observedBytes: error.observedBytes, label: error.label, filePath, }) return { success: false, error: error.label === 'parsed file output' ? getParsedOutputTooLargeMessage(error.maxBytes) : `File is too large to parse safely. Maximum supported local file size is ${prettySize( error.maxBytes )}.`, filePath, } } return { success: false, error: `Error processing local file: ${(error as Error).message}`, filePath, } } } /** * Handle a PDF buffer directly in memory */ async function handlePdfBuffer( fileBuffer: Buffer, filename: string, fileType?: string, originalPath?: string, maxParsedOutputBytes?: number ): Promise { try { logger.info(`Parsing PDF in memory: ${filename}`) const result = await parseBufferAsPdf(fileBuffer) const content = result.content || createPdfFallbackMessage(result.metadata?.pageCount || 0, fileBuffer.length, originalPath) const limitedContent = assertParsedContentWithinLimit(content, maxParsedOutputBytes) return { success: true, content: limitedContent, filePath: originalPath || filename, metadata: { fileType: fileType || 'application/pdf', size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } catch (error) { if (isPayloadSizeLimitError(error)) throw error logger.error('Failed to parse PDF in memory:', error) const content = createPdfFailureMessage( 0, fileBuffer.length, originalPath || filename, (error as Error).message ) return { success: true, content, filePath: originalPath || filename, metadata: { fileType: fileType || 'application/pdf', size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } } /** * Handle a CSV buffer directly in memory */ async function handleCsvBuffer( fileBuffer: Buffer, filename: string, fileType?: string, originalPath?: string, maxParsedOutputBytes?: number ): Promise { try { logger.info(`Parsing CSV in memory: ${filename}`) const { parseBuffer } = await import('@/lib/file-parsers') const result = await parseBuffer(fileBuffer, 'csv') return { success: true, content: assertParsedContentWithinLimit(result.content, maxParsedOutputBytes), filePath: originalPath || filename, metadata: { fileType: fileType || 'text/csv', size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } catch (error) { if (isPayloadSizeLimitError(error)) throw error logger.error('Failed to parse CSV in memory:', error) return { success: false, error: `Failed to parse CSV: ${(error as Error).message}`, filePath: originalPath || filename, metadata: { fileType: 'text/csv', size: 0, hash: '', processingTime: 0, }, } } } /** * Handle a generic text file buffer in memory */ async function handleGenericTextBuffer( fileBuffer: Buffer, filename: string, extension: string, fileType?: string, originalPath?: string, maxParsedOutputBytes?: number ): Promise { try { logger.info(`Parsing text file in memory: ${filename}`) try { const { parseBuffer, isSupportedFileType } = await import('@/lib/file-parsers') if (isSupportedFileType(extension)) { const result = await parseBuffer(fileBuffer, extension) return { success: true, content: assertParsedContentWithinLimit(result.content, maxParsedOutputBytes), filePath: originalPath || filename, metadata: { fileType: fileType || getMimeTypeFromExtension(extension), size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } } catch (parserError) { if (isPayloadSizeLimitError(parserError)) throw parserError logger.warn('Specialized parser failed, falling back to generic parsing:', parserError) } const content = fileBuffer.toString('utf-8') const limitedContent = assertParsedContentWithinLimit(content, maxParsedOutputBytes) return { success: true, content: limitedContent, filePath: originalPath || filename, metadata: { fileType: fileType || getMimeTypeFromExtension(extension), size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } catch (error) { if (isPayloadSizeLimitError(error)) throw error logger.error('Failed to parse text file in memory:', error) return { success: false, error: `Failed to parse file: ${(error as Error).message}`, filePath: originalPath || filename, metadata: { fileType: 'text/plain', size: 0, hash: '', processingTime: 0, }, } } } /** * Handle a generic binary buffer */ function handleGenericBuffer( fileBuffer: Buffer, filename: string, extension: string, fileType?: string, maxParsedOutputBytes?: number ): ParseResult { const normalizedExtension = extension.toLowerCase() const content = !BINARY_EXTENSIONS.has(normalizedExtension) && isLikelyTextBuffer(fileBuffer) ? assertParsedContentWithinLimit(fileBuffer.toString('utf-8'), maxParsedOutputBytes) : `[Binary ${normalizedExtension.toUpperCase()} file - ${fileBuffer.length} bytes]` return { success: true, content, filePath: filename, metadata: { fileType: fileType || getMimeTypeFromExtension(extension), size: fileBuffer.length, hash: createHash('md5').update(fileBuffer).digest('hex'), processingTime: 0, }, } } /** * Parse a PDF buffer */ async function parseBufferAsPdf(buffer: Buffer) { try { const { PdfParser } = await import('@/lib/file-parsers/pdf-parser') const parser = new PdfParser() logger.info('Using main PDF parser for buffer') return await parser.parseBuffer(buffer) } catch (error) { throw new Error(`PDF parsing failed: ${(error as Error).message}`) } } /** * Format bytes to human readable size */ function prettySize(bytes: number): string { if (bytes === 0) return '0 Bytes' const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) return `${Number.parseFloat((bytes / 1024 ** i).toFixed(2))} ${sizes[i]}` } /** * Create a formatted message for PDF content */ function createPdfFallbackMessage(pageCount: number, size: number, path?: string): string { const formattedPath = path || 'Unknown path' return `PDF document - ${pageCount} page(s), ${prettySize(size)} Path: ${formattedPath} This file appears to be a PDF document that could not be fully processed as text. Please use a PDF viewer for best results.` } /** * Create error message for PDF parsing failure and make it more readable */ function createPdfFailureMessage( pageCount: number, size: number, path: string, error: string ): string { return `PDF document - Processing failed, ${prettySize(size)} Path: ${path} Error: ${error} This file appears to be a PDF document that could not be processed. Please use a PDF viewer for best results.` }