Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/docs/content/docs/en/tools/onedrive.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ In Sim, the OneDrive integration enables your agents to directly interact with y

## Usage Instructions

Integrate OneDrive into the workflow. Can create, upload, and list files.
Integrate OneDrive into the workflow. Can create text and Excel files, upload files, and list files.



Expand All @@ -68,6 +68,7 @@ Upload a file to OneDrive
| `fileName` | string | Yes | The name of the file to upload |
| `file` | file | No | The file to upload \(binary\) |
| `content` | string | No | The text content to upload \(if no file is provided\) |
| `mimeType` | string | No | The MIME type of the file to create \(e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx\) |
| `folderSelector` | string | No | Select the folder to upload the file to |
| `manualFolderId` | string | No | Manually entered folder ID \(advanced mode\) |

Expand Down
297 changes: 242 additions & 55 deletions apps/sim/app/api/tools/onedrive/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server'
import * as XLSX from 'xlsx'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
Expand All @@ -14,8 +15,11 @@ const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0'
const OneDriveUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
fileName: z.string().min(1, 'File name is required'),
file: z.any(), // UserFile object
file: z.any().optional(), // UserFile object (optional for blank Excel creation)
folderId: z.string().optional().nullable(),
mimeType: z.string().optional(),
// Optional Excel write-after-create inputs
values: z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))).optional(),
})

export async function POST(request: NextRequest) {
Expand All @@ -42,17 +46,30 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validatedData = OneDriveUploadSchema.parse(body)

logger.info(`[${requestId}] Uploading file to OneDrive`, {
fileName: validatedData.fileName,
folderId: validatedData.folderId || 'root',
})
let fileBuffer: Buffer
let mimeType: string

// Check if we're creating a blank Excel file
const isExcelCreation =
validatedData.mimeType ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && !validatedData.file

if (isExcelCreation) {
// Create a blank Excel workbook

// Handle array or single file
const rawFile = validatedData.file
let fileToProcess
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet([[]])
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')

if (Array.isArray(rawFile)) {
if (rawFile.length === 0) {
// Generate XLSX file as buffer
const xlsxBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
fileBuffer = Buffer.from(xlsxBuffer)
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
} else {
// Handle regular file upload
const rawFile = validatedData.file

if (!rawFile) {
return NextResponse.json(
{
success: false,
Expand All @@ -61,40 +78,51 @@ export async function POST(request: NextRequest) {
{ status: 400 }
)
}
fileToProcess = rawFile[0]
} else {
fileToProcess = rawFile
}

// Convert to UserFile format
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process file',
},
{ status: 400 }
)
}
let fileToProcess
if (Array.isArray(rawFile)) {
if (rawFile.length === 0) {
return NextResponse.json(
{
success: false,
error: 'No file provided',
},
{ status: 400 }
)
}
fileToProcess = rawFile[0]
} else {
fileToProcess = rawFile
}

logger.info(`[${requestId}] Downloading file from storage: ${userFile.key}`)
// Convert to UserFile format
let userFile
try {
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process file',
},
{ status: 400 }
)
}

let fileBuffer: Buffer
try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download file from storage:`, error)
return NextResponse.json(
{
success: false,
error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}

try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download file from storage:`, error)
return NextResponse.json(
{
success: false,
error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
mimeType = userFile.type || 'application/octet-stream'
}

const maxSize = 250 * 1024 * 1024 // 250MB
Expand All @@ -110,7 +138,11 @@ export async function POST(request: NextRequest) {
)
}

const fileName = validatedData.fileName || userFile.name
// Ensure file name has correct extension for Excel files
let fileName = validatedData.fileName
if (isExcelCreation && !fileName.endsWith('.xlsx')) {
fileName = `${fileName.replace(/\.[^.]*$/, '')}.xlsx`
}

let uploadUrl: string
const folderId = validatedData.folderId?.trim()
Expand All @@ -121,10 +153,6 @@ export async function POST(request: NextRequest) {
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/root:/${encodeURIComponent(fileName)}:/content`
}

logger.info(`[${requestId}] Uploading to OneDrive: ${uploadUrl}`)

const mimeType = userFile.type || 'application/octet-stream'

const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
headers: {
Expand All @@ -136,11 +164,6 @@ export async function POST(request: NextRequest) {

if (!uploadResponse.ok) {
const errorText = await uploadResponse.text()
logger.error(`[${requestId}] OneDrive upload failed:`, {
status: uploadResponse.status,
statusText: uploadResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
success: false,
Expand All @@ -153,11 +176,174 @@ export async function POST(request: NextRequest) {

const fileData = await uploadResponse.json()

logger.info(`[${requestId}] File uploaded successfully to OneDrive`, {
fileId: fileData.id,
fileName: fileData.name,
size: fileData.size,
})
// If this is an Excel creation and values were provided, write them using the Excel API
let excelWriteResult: any | undefined
const shouldWriteExcelContent =
isExcelCreation && Array.isArray(validatedData.values) && validatedData.values.length > 0

if (shouldWriteExcelContent) {
try {
// Create a workbook session to ensure reliability and persistence of changes
let workbookSessionId: string | undefined
const sessionResp = await fetch(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/createSession`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ persistChanges: true }),
}
)

if (sessionResp.ok) {
const sessionData = await sessionResp.json()
workbookSessionId = sessionData?.id
}

// Determine the first worksheet name
let sheetName = 'Sheet1'
try {
const listUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
fileData.id
)}/workbook/worksheets?$select=name&$orderby=position&$top=1`
const listResp = await fetch(listUrl, {
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
...(workbookSessionId ? { 'workbook-session-id': workbookSessionId } : {}),
},
})
if (listResp.ok) {
const listData = await listResp.json()
const firstSheetName = listData?.value?.[0]?.name
if (firstSheetName) {
sheetName = firstSheetName
}
} else {
const listErr = await listResp.text()
logger.warn(`[${requestId}] Failed to list worksheets, using default Sheet1`, {
status: listResp.status,
error: listErr,
})
}
} catch (listError) {
logger.warn(`[${requestId}] Error listing worksheets, using default Sheet1`, listError)
}

let processedValues: any = validatedData.values || []

if (
Array.isArray(processedValues) &&
processedValues.length > 0 &&
typeof processedValues[0] === 'object' &&
!Array.isArray(processedValues[0])
) {
const ws = XLSX.utils.json_to_sheet(processedValues)
processedValues = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' })
}

const rowsCount = processedValues.length
const colsCount = Math.max(...processedValues.map((row: any[]) => row.length), 0)
processedValues = processedValues.map((row: any[]) => {
const paddedRow = [...row]
while (paddedRow.length < colsCount) paddedRow.push('')
return paddedRow
})

// Compute concise end range from A1 and matrix size (no network round-trip)
const indexToColLetters = (index: number): string => {
let n = index
let s = ''
while (n > 0) {
const rem = (n - 1) % 26
s = String.fromCharCode(65 + rem) + s
n = Math.floor((n - 1) / 26)
}
return s
}

const endColLetters = colsCount > 0 ? indexToColLetters(colsCount) : 'A'
const endRow = rowsCount > 0 ? rowsCount : 1
const computedRangeAddress = `A1:${endColLetters}${endRow}`

const url = new URL(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
fileData.id
)}/workbook/worksheets('${encodeURIComponent(
sheetName
)}')/range(address='${encodeURIComponent(computedRangeAddress)}')`
)

const excelWriteResponse = await fetch(url.toString(), {
method: 'PATCH',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/json',
...(workbookSessionId ? { 'workbook-session-id': workbookSessionId } : {}),
},
body: JSON.stringify({ values: processedValues }),
})

if (!excelWriteResponse || !excelWriteResponse.ok) {
const errorText = excelWriteResponse ? await excelWriteResponse.text() : 'no response'
logger.error(`[${requestId}] Excel content write failed`, {
status: excelWriteResponse?.status,
statusText: excelWriteResponse?.statusText,
error: errorText,
})
// Do not fail the entire request; return upload success with write error details
excelWriteResult = {
success: false,
error: `Excel write failed: ${excelWriteResponse?.statusText || 'unknown'}`,
details: errorText,
}
} else {
const writeData = await excelWriteResponse.json()
// The Range PATCH returns a Range object; log address and values length
const addr = writeData.address || writeData.addressLocal
const v = writeData.values || []
excelWriteResult = {
success: true,
updatedRange: addr,
updatedRows: Array.isArray(v) ? v.length : undefined,
updatedColumns: Array.isArray(v) && v[0] ? v[0].length : undefined,
updatedCells: Array.isArray(v) && v[0] ? v.length * (v[0] as any[]).length : undefined,
}
}

// Attempt to close the workbook session if one was created
if (workbookSessionId) {
try {
const closeResp = await fetch(
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/closeSession`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'workbook-session-id': workbookSessionId,
},
}
)
if (!closeResp.ok) {
const closeText = await closeResp.text()
logger.warn(`[${requestId}] Failed to close Excel session`, {
status: closeResp.status,
error: closeText,
})
}
} catch (closeErr) {
logger.warn(`[${requestId}] Error closing Excel session`, closeErr)
}
}
} catch (err) {
logger.error(`[${requestId}] Exception during Excel content write`, err)
excelWriteResult = {
success: false,
error: err instanceof Error ? err.message : 'Unknown error during Excel write',
}
}
}

return NextResponse.json({
success: true,
Expand All @@ -173,6 +359,7 @@ export async function POST(request: NextRequest) {
modifiedTime: fileData.lastModifiedDateTime,
parentReference: fileData.parentReference,
},
...(excelWriteResult ? { excelWriteResult } : {}),
},
})
} catch (error) {
Expand Down
Loading