fix dropbox

This commit is contained in:
Vikhyath Mondreti
2026-02-03 15:50:50 -08:00
parent 2d96ac55db
commit aa1b158b26
4 changed files with 183 additions and 54 deletions

View File

@@ -0,0 +1,140 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('DropboxUploadAPI')
/**
* Escapes non-ASCII characters in JSON string for HTTP header safety.
* Dropbox API requires characters 0x7F and all non-ASCII to be escaped as \uXXXX.
*/
function httpHeaderSafeJson(value: object): string {
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => {
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
})
}
const DropboxUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
path: z.string().min(1, 'Destination path is required'),
file: FileInputSchema.optional().nullable(),
// Legacy field for backwards compatibility
fileContent: z.string().optional().nullable(),
fileName: z.string().optional().nullable(),
mode: z.enum(['add', 'overwrite']).optional().nullable(),
autorename: z.boolean().optional().nullable(),
mute: z.boolean().optional().nullable(),
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated Dropbox upload request via ${authResult.authType}`)
const body = await request.json()
const validatedData = DropboxUploadSchema.parse(body)
let fileBuffer: Buffer
let fileName: string
// Prefer UserFile input, fall back to legacy base64 string
if (validatedData.file) {
// Process UserFile input
const userFiles = processFilesToUserFiles([validatedData.file], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
}
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (validatedData.fileContent) {
// Legacy: base64 string input (backwards compatibility)
logger.info(`[${requestId}] Using legacy base64 content input`)
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
fileName = validatedData.fileName || 'file'
} else {
return NextResponse.json(
{ success: false, error: 'File or file content is required' },
{ status: 400 }
)
}
// Determine final path
let finalPath = validatedData.path
if (finalPath.endsWith('/')) {
finalPath = `${finalPath}${fileName}`
}
logger.info(`[${requestId}] Uploading to Dropbox: ${finalPath} (${fileBuffer.length} bytes)`)
const dropboxApiArg = {
path: finalPath,
mode: validatedData.mode || 'add',
autorename: validatedData.autorename ?? true,
mute: validatedData.mute ?? false,
}
const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/octet-stream',
'Dropbox-API-Arg': httpHeaderSafeJson(dropboxApiArg),
},
body: fileBuffer,
})
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error_summary || data.error?.message || 'Failed to upload file'
logger.error(`[${requestId}] Dropbox API error:`, { status: response.status, data })
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
}
logger.info(`[${requestId}] File uploaded successfully to ${data.path_display}`)
return NextResponse.json({
success: true,
output: {
file: data,
},
})
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Validation error:`, error.errors)
return NextResponse.json(
{ success: false, error: error.errors[0]?.message || 'Validation failed' },
{ status: 400 }
)
}
logger.error(`[${requestId}] Unexpected error:`, error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}

View File

@@ -64,7 +64,7 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'fileContent',
canonicalParamId: 'file',
placeholder: 'Upload file to send to Dropbox',
mode: 'basic',
multiple: false,
@@ -72,10 +72,10 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
condition: { field: 'operation', value: 'dropbox_upload' },
},
{
id: 'fileContent',
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'fileContent',
canonicalParamId: 'file',
placeholder: 'Reference file from previous blocks',
mode: 'advanced',
required: true,
@@ -319,7 +319,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// Normalize file input for upload operation
// normalizeFileInput handles JSON stringified values from advanced mode
if (params.fileContent) {
if (params.file) {
params.file = normalizeFileInput(params.file, { single: true })
}
// Legacy: also check fileContent for backwards compatibility
if (params.fileContent && !params.file) {
params.fileContent = normalizeFileInput(params.fileContent, { single: true })
}
@@ -358,7 +362,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
autorename: { type: 'boolean', description: 'Auto-rename on conflict' },
// Upload inputs
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
fileContent: { type: 'json', description: 'File reference or UserFile object' },
file: { type: 'json', description: 'File to upload (UserFile object)' },
fileRef: { type: 'json', description: 'File reference from previous block' },
fileContent: { type: 'string', description: 'Legacy: base64 encoded file content' },
fileName: { type: 'string', description: 'Optional filename' },
mode: { type: 'string', description: 'Write mode: add or overwrite' },
mute: { type: 'boolean', description: 'Mute notifications' },

View File

@@ -71,7 +71,9 @@ export interface DropboxBaseParams {
export interface DropboxUploadParams extends DropboxBaseParams {
path: string
fileContent: string | UserFileLike
file?: UserFileLike
// Legacy field for backwards compatibility
fileContent?: string
fileName?: string
mode?: 'add' | 'overwrite'
autorename?: boolean

View File

@@ -1,17 +1,6 @@
import { extractBase64FromFileInput } from '@/lib/core/utils/user-file'
import type { DropboxUploadParams, DropboxUploadResponse } from '@/tools/dropbox/types'
import type { ToolConfig } from '@/tools/types'
/**
* Escapes non-ASCII characters in JSON string for HTTP header safety.
* Dropbox API requires characters 0x7F and all non-ASCII to be escaped as \uXXXX.
*/
function httpHeaderSafeJson(value: object): string {
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => {
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
})
}
export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadResponse> = {
id: 'dropbox_upload',
name: 'Dropbox Upload File',
@@ -31,11 +20,18 @@ export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadRes
description:
'The path in Dropbox where the file should be saved (e.g., /folder/document.pdf)',
},
fileContent: {
type: 'json',
required: true,
file: {
type: 'file',
required: false,
visibility: 'user-or-llm',
description: 'The file to upload (UserFile object or base64 string)',
description: 'The file to upload (UserFile object)',
},
// Legacy field for backwards compatibility - hidden from UI
fileContent: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Legacy: base64 encoded file content',
},
fileName: {
type: 'string',
@@ -64,52 +60,37 @@ export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadRes
},
request: {
url: 'https://content.dropboxapi.com/2/files/upload',
url: '/api/tools/dropbox/upload',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Missing access token for Dropbox API request')
}
const dropboxApiArg = {
path: params.path,
mode: params.mode || 'add',
autorename: params.autorename ?? true,
mute: params.mute ?? false,
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/octet-stream',
'Dropbox-API-Arg': httpHeaderSafeJson(dropboxApiArg),
}
},
body: (params) => {
const base64Content = extractBase64FromFileInput(params.fileContent)
if (!base64Content) {
throw new Error('File Content cannot be extracted')
}
// Decode base64 to raw binary bytes - Dropbox expects raw binary, not base64 text
return Buffer.from(base64Content, 'base64')
},
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
path: params.path,
file: params.file,
fileContent: params.fileContent,
fileName: params.fileName,
mode: params.mode,
autorename: params.autorename,
mute: params.mute,
}),
},
transformResponse: async (response, params) => {
transformResponse: async (response): Promise<DropboxUploadResponse> => {
const data = await response.json()
if (!response.ok) {
if (!data.success) {
return {
success: false,
error: data.error_summary || data.error?.message || 'Failed to upload file',
error: data.error || 'Failed to upload file',
output: {},
}
}
return {
success: true,
output: {
file: data,
},
output: data.output,
}
},