import { createLogger } from '@sim/logger' import { getBaseUrl } from '@/lib/core/utils/urls' import { deleteFile, downloadFile, generatePresignedDownloadUrl, generatePresignedUploadUrl, uploadFile, } from '@/lib/uploads/core/storage-service' import type { PresignedUrlResponse } from '@/lib/uploads/shared/types' import { isImageFileType } from '@/lib/uploads/utils/file-utils' const logger = createLogger('CopilotFileManager') const SUPPORTED_FILE_TYPES = [ // Images 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', // Documents 'application/pdf', 'text/plain', 'text/csv', 'text/markdown', 'text/html', 'application/json', 'application/xml', 'text/xml', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/x-pptxgenjs', 'text/x-docxjs', 'text/x-python-pdf', 'text/x-python-xlsx', ] /** * Check if a MIME type is supported for copilot attachments */ export function isSupportedFileType(mimeType: string): boolean { return SUPPORTED_FILE_TYPES.includes(mimeType.toLowerCase()) } interface CopilotFileAttachment { key: string filename: string media_type: string } export interface GenerateCopilotUploadUrlOptions { fileName: string contentType: string fileSize: number userId: string expirationSeconds?: number } export interface CopilotStoredFile { id: string key: string context: 'copilot' name: string url: string size: number type: string mimeType: string } /** * Generate a presigned URL for copilot file upload * * Images and document files are allowed for copilot uploads. * Requires authenticated user session. * * @param options Upload URL generation options * @returns Presigned URL response with upload URL and file key * @throws Error if file type is unsupported or user is not authenticated */ export async function generateCopilotUploadUrl( options: GenerateCopilotUploadUrlOptions ): Promise { const { fileName, contentType, fileSize, userId, expirationSeconds = 3600 } = options if (!userId?.trim()) { throw new Error('Authenticated user session is required for copilot uploads') } if (!isSupportedFileType(contentType) && !isImageFileType(contentType)) { throw new Error( 'Unsupported file type. Allowed: images (JPEG, PNG, GIF, WebP), PDF, and text files (TXT, CSV, MD, HTML, JSON, XML).' ) } const presignedUrlResponse = await generatePresignedUploadUrl({ fileName, contentType, fileSize, context: 'copilot', userId, expirationSeconds, }) logger.info(`Generated copilot upload URL for: ${fileName}`, { key: presignedUrlResponse.key, userId, }) return presignedUrlResponse } export async function uploadCopilotFile(options: { buffer: Buffer fileName: string contentType: string userId: string }): Promise { const fileInfo = await uploadFile({ file: options.buffer, fileName: options.fileName, contentType: options.contentType, context: 'copilot', metadata: { userId: options.userId, originalName: options.fileName, uploadedAt: new Date().toISOString(), purpose: 'copilot-tool-output', }, }) const url = `${getBaseUrl()}${fileInfo.path}` logger.info(`Stored copilot tool output: ${options.fileName}`, { key: fileInfo.key, size: fileInfo.size, userId: options.userId, }) return { id: fileInfo.key, key: fileInfo.key, context: 'copilot', name: fileInfo.name, url, size: fileInfo.size, type: fileInfo.type, mimeType: fileInfo.type, } } /** * Download a copilot file from storage * * Uses the unified storage service with explicit copilot context. * Handles S3, Azure Blob, and local storage automatically. * * @param key File storage key * @returns File buffer * @throws Error if file not found or download fails */ export async function downloadCopilotFile(key: string): Promise { try { const fileBuffer = await downloadFile({ key, context: 'copilot', }) logger.info(`Successfully downloaded copilot file: ${key}`, { size: fileBuffer.length, }) return fileBuffer } catch (error) { logger.error(`Failed to download copilot file: ${key}`, error) throw error } } /** * Process copilot file attachments for chat messages * * Downloads files from storage and validates they are supported types. * Skips unsupported files with a warning. * * @param attachments Array of file attachments * @param requestId Request identifier for logging * @returns Array of buffers for successfully downloaded files */ export async function processCopilotAttachments( attachments: CopilotFileAttachment[], requestId: string ): Promise> { const results: Array<{ buffer: Buffer; attachment: CopilotFileAttachment }> = [] for (const attachment of attachments) { try { if (!isSupportedFileType(attachment.media_type)) { logger.warn(`[${requestId}] Unsupported file type: ${attachment.media_type}`) continue } const buffer = await downloadCopilotFile(attachment.key) results.push({ buffer, attachment }) } catch (error) { logger.error(`[${requestId}] Failed to process file ${attachment.filename}:`, error) } } logger.info(`Successfully processed ${results.length}/${attachments.length} attachments`, { requestId, }) return results } /** * Generate a presigned download URL for a copilot file * * @param key File storage key * @param expirationSeconds Time in seconds until URL expires (default: 1 hour) * @returns Presigned download URL */ export async function generateCopilotDownloadUrl( key: string, expirationSeconds = 3600 ): Promise { const downloadUrl = await generatePresignedDownloadUrl(key, 'copilot', expirationSeconds) logger.info(`Generated copilot download URL for: ${key}`) return downloadUrl } /** * Delete a copilot file from storage * * @param key File storage key */ export async function deleteCopilotFile(key: string): Promise { await deleteFile({ key, context: 'copilot', }) logger.info(`Successfully deleted copilot file: ${key}`) }