import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { sharepointUploadContract } from '@/lib/api/contracts/tools/microsoft' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } 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 type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types' import type { SharepointSkippedFile, SharepointUploadError } from '@/tools/sharepoint/types' export const dynamic = 'force-dynamic' const logger = createLogger('SharepointUploadAPI') const MAX_SHAREPOINT_UPLOAD_BYTES = 250 * 1024 * 1024 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 SharePoint upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required', }, { status: 401 } ) } logger.info( `[${requestId}] Authenticated SharePoint upload request via ${authResult.authType}`, { userId: authResult.userId, } ) const parsed = await parseRequest(sharepointUploadContract, request, {}) if (!parsed.success) return parsed.response const validatedData = parsed.data.body logger.info(`[${requestId}] Uploading files to SharePoint`, { siteId: validatedData.siteId, driveId: validatedData.driveId, folderPath: validatedData.folderPath, hasFiles: !!(validatedData.files && validatedData.files.length > 0), fileCount: validatedData.files?.length || 0, }) if (!validatedData.files || validatedData.files.length === 0) { return NextResponse.json( { success: false, error: 'At least one file is required for upload', }, { status: 400 } ) } const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) if (userFiles.length === 0) { return NextResponse.json( { success: false, error: 'No valid files to upload', }, { status: 400 } ) } const siteId = validatedData.siteId.trim() || 'root' const driveId = validatedData.driveId?.trim() || null const uploadedFiles: MicrosoftGraphDriveItem[] = [] const skippedFiles: SharepointSkippedFile[] = [] const errors: SharepointUploadError[] = [] for (const userFile of userFiles) { const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) if (denied) return denied logger.info(`[${requestId}] Uploading file: ${userFile.name}`) let buffer: Buffer let downloadedContentType = '' try { const result = await downloadServableFileFromStorage(userFile, requestId, logger) buffer = result.buffer downloadedContentType = result.contentType } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady throw error } const fileName = validatedData.fileName || userFile.name const folderPath = validatedData.folderPath?.trim() || '' const fileSizeMB = buffer.length / (1024 * 1024) if (buffer.length > MAX_SHAREPOINT_UPLOAD_BYTES) { logger.warn( `[${requestId}] File ${fileName} is ${fileSizeMB.toFixed(2)}MB, exceeds 250MB limit` ) skippedFiles.push({ name: fileName, size: buffer.length, limit: MAX_SHAREPOINT_UPLOAD_BYTES, reason: 'File exceeds the 250 MB Microsoft Graph small upload limit', }) continue } let uploadPath = '' if (folderPath) { const normalizedPath = folderPath.startsWith('/') ? folderPath : `/${folderPath}` const cleanPath = normalizedPath.endsWith('/') ? normalizedPath.slice(0, -1) : normalizedPath uploadPath = `${cleanPath}/${fileName}` } else { uploadPath = `/${fileName}` } const encodedPath = uploadPath .split('/') .map((segment) => (segment ? encodeURIComponent(segment) : '')) .join('/') const uploadUrl = driveId ? `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/root:${encodedPath}:/content` : `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/drive/root:${encodedPath}:/content` logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', headers: { Authorization: `Bearer ${validatedData.accessToken}`, 'Content-Type': downloadedContentType || userFile.type || 'application/octet-stream', }, body: buffer, }, 'uploadUrl' ) if (!uploadResponse.ok) { const errorData = await uploadResponse.json().catch(() => ({})) logger.error(`[${requestId}] Failed to upload file ${fileName}:`, errorData) if (uploadResponse.status === 409) { // File exists - retry with conflict behavior set to replace logger.warn(`[${requestId}] File ${fileName} already exists, retrying with replace`) const replaceUrl = `${uploadUrl}?@microsoft.graph.conflictBehavior=replace` const replaceResponse = await secureFetchWithValidation( replaceUrl, { method: 'PUT', headers: { Authorization: `Bearer ${validatedData.accessToken}`, 'Content-Type': downloadedContentType || userFile.type || 'application/octet-stream', }, body: buffer, }, 'replaceUrl' ) if (!replaceResponse.ok) { const replaceErrorData = (await replaceResponse.json().catch(() => ({}))) as { error?: { message?: string } } logger.error(`[${requestId}] Failed to replace file ${fileName}:`, replaceErrorData) errors.push({ name: fileName, status: replaceResponse.status, error: replaceErrorData.error?.message || `Failed to replace file: ${fileName}`, }) continue } const replaceData = (await replaceResponse.json()) as { id: string name: string webUrl: string size: number createdDateTime: string lastModifiedDateTime: string } logger.info(`[${requestId}] File replaced successfully: ${fileName}`) uploadedFiles.push({ id: replaceData.id, name: replaceData.name, webUrl: replaceData.webUrl, size: replaceData.size, createdDateTime: replaceData.createdDateTime, lastModifiedDateTime: replaceData.lastModifiedDateTime, }) continue } errors.push({ name: fileName, status: uploadResponse.status, error: (errorData as { error?: { message?: string } }).error?.message || `Failed to upload file: ${fileName}`, }) continue } const uploadData = (await uploadResponse.json()) as MicrosoftGraphDriveItem logger.info(`[${requestId}] File uploaded successfully: ${fileName}`) uploadedFiles.push({ id: uploadData.id, name: uploadData.name, webUrl: uploadData.webUrl, size: uploadData.size, createdDateTime: uploadData.createdDateTime, lastModifiedDateTime: uploadData.lastModifiedDateTime, }) } if (uploadedFiles.length === 0) { return NextResponse.json({ success: false, error: 'No files were uploaded successfully', output: { uploadedFiles, fileCount: 0, skippedFiles, skippedCount: skippedFiles.length, errors, }, }) } logger.info(`[${requestId}] Completed SharePoint upload`, { uploadedCount: uploadedFiles.length, skippedCount: skippedFiles.length, errorCount: errors.length, }) return NextResponse.json({ success: true, output: { uploadedFiles, fileCount: uploadedFiles.length, skippedFiles, skippedCount: skippedFiles.length, errors, }, }) } catch (error) { logger.error(`[${requestId}] Error uploading files to SharePoint:`, error) return NextResponse.json( { success: false, error: toError(error).message, }, { status: 500 } ) } })