mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-03 19:24:57 -05:00
fix dropbox
This commit is contained in:
140
apps/sim/app/api/tools/dropbox/upload/route.ts
Normal file
140
apps/sim/app/api/tools/dropbox/upload/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user