import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' import { type NextRequest, NextResponse } from 'next/server' import { googleDriveUploadContract } from '@/lib/api/contracts/tools/google' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' import { GOOGLE_WORKSPACE_MIME_TYPES, handleSheetsFormat, SOURCE_MIME_TYPES, } from '@/tools/google_drive/utils' export const dynamic = 'force-dynamic' const logger = createLogger('GoogleDriveUploadAPI') const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/upload/drive/v3/files' /** * Build multipart upload body for Google Drive API */ function buildMultipartBody( metadata: Record, fileBuffer: Buffer, mimeType: string, boundary: string ): string { const parts: string[] = [] parts.push(`--${boundary}`) parts.push('Content-Type: application/json; charset=UTF-8') parts.push('') parts.push(JSON.stringify(metadata)) parts.push(`--${boundary}`) parts.push(`Content-Type: ${mimeType}`) parts.push('Content-Transfer-Encoding: base64') parts.push('') parts.push(fileBuffer.toString('base64')) parts.push(`--${boundary}--`) return parts.join('\r\n') } export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required', }, { status: 401 } ) } logger.info( `[${requestId}] Authenticated Google Drive upload request via ${authResult.authType}`, { userId: authResult.userId, } ) const parsed = await parseRequest(googleDriveUploadContract, request, {}) if (!parsed.success) return parsed.response const validatedData = parsed.data.body logger.info(`[${requestId}] Uploading file to Google Drive`, { fileName: validatedData.fileName, mimeType: validatedData.mimeType, folderId: validatedData.folderId, hasFile: !!validatedData.file, }) if (!validatedData.file) { return NextResponse.json( { success: false, error: 'No file provided. Use the text content field for text-only uploads.', }, { status: 400 } ) } // Process file - convert to UserFile format if needed const fileData = validatedData.file let userFile try { userFile = processSingleFileToUserFile(fileData, requestId, logger) } catch (error) { return NextResponse.json( { success: false, error: getErrorMessage(error, 'Failed to process file'), }, { status: 400 } ) } logger.info(`[${requestId}] Downloading file from storage`, { fileName: userFile.name, key: userFile.key, size: userFile.size, }) const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) if (denied) return denied let fileBuffer: Buffer let downloadedContentType = '' try { const result = await downloadServableFileFromStorage(userFile, requestId, logger) fileBuffer = result.buffer downloadedContentType = result.contentType } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady logger.error(`[${requestId}] Failed to download file:`, error) return NextResponse.json( { success: false, error: `Failed to download file: ${getErrorMessage(error, 'Unknown error')}`, }, { status: 500 } ) } let uploadMimeType = validatedData.mimeType || downloadedContentType || userFile.type || 'application/octet-stream' const requestedMimeType = validatedData.mimeType || downloadedContentType || userFile.type || 'application/octet-stream' if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) { uploadMimeType = SOURCE_MIME_TYPES[requestedMimeType] || 'text/plain' logger.info(`[${requestId}] Converting to Google Workspace type`, { requestedMimeType, uploadMimeType, }) } if (requestedMimeType === 'application/vnd.google-apps.spreadsheet') { try { const textContent = fileBuffer.toString('utf-8') const { csv } = handleSheetsFormat(textContent) if (csv !== undefined) { fileBuffer = Buffer.from(csv, 'utf-8') uploadMimeType = 'text/csv' logger.info(`[${requestId}] Converted to CSV for Google Sheets upload`) } } catch (error) { logger.warn(`[${requestId}] Could not convert to CSV, uploading as-is:`, error) } } const metadata: { name: string mimeType: string parents?: string[] } = { name: validatedData.fileName, mimeType: requestedMimeType, } if (validatedData.folderId && validatedData.folderId.trim() !== '') { metadata.parents = [validatedData.folderId.trim()] } const boundary = `boundary_${Date.now()}_${generateShortId(7)}` const multipartBody = buildMultipartBody(metadata, fileBuffer, uploadMimeType, boundary) logger.info(`[${requestId}] Uploading to Google Drive via multipart upload`, { fileName: validatedData.fileName, size: fileBuffer.length, uploadMimeType, requestedMimeType, }) const uploadResponse = await fetch( `${GOOGLE_DRIVE_API_BASE}?uploadType=multipart&supportsAllDrives=true`, { method: 'POST', headers: { Authorization: `Bearer ${validatedData.accessToken}`, 'Content-Type': `multipart/related; boundary=${boundary}`, 'Content-Length': Buffer.byteLength(multipartBody, 'utf-8').toString(), }, body: multipartBody, } ) if (!uploadResponse.ok) { const errorText = await uploadResponse.text() logger.error(`[${requestId}] Google Drive API error:`, { status: uploadResponse.status, statusText: uploadResponse.statusText, error: errorText, }) return NextResponse.json( { success: false, error: `Google Drive API error: ${uploadResponse.statusText}`, }, { status: uploadResponse.status } ) } const uploadData = await uploadResponse.json() const fileId = uploadData.id logger.info(`[${requestId}] File uploaded successfully`, { fileId }) if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) { logger.info(`[${requestId}] Updating file name to ensure it persists after conversion`) const updateNameResponse = await fetch( `https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true`, { method: 'PATCH', headers: { Authorization: `Bearer ${validatedData.accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: validatedData.fileName, }), } ) if (!updateNameResponse.ok) { logger.warn( `[${requestId}] Failed to update filename after conversion, but content was uploaded` ) } } const finalFileResponse = await fetch( `https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true&fields=id,name,mimeType,webViewLink,webContentLink,size,createdTime,modifiedTime,parents`, { headers: { Authorization: `Bearer ${validatedData.accessToken}`, }, } ) const finalFile = await finalFileResponse.json() logger.info(`[${requestId}] Upload complete`, { fileId: finalFile.id, fileName: finalFile.name, webViewLink: finalFile.webViewLink, }) return NextResponse.json({ success: true, output: { file: { id: finalFile.id, name: finalFile.name, mimeType: finalFile.mimeType, webViewLink: finalFile.webViewLink, webContentLink: finalFile.webContentLink, size: finalFile.size, createdTime: finalFile.createdTime, modifiedTime: finalFile.modifiedTime, parents: finalFile.parents, }, }, }) } catch (error) { logger.error(`[${requestId}] Error uploading file to Google Drive:`, error) return NextResponse.json( { success: false, error: getErrorMessage(error, 'Internal server error'), }, { status: 500 } ) } })