Skip to content

Commit aa1b158

Browse files
committed
fix dropbox
1 parent 2d96ac5 commit aa1b158

4 files changed

Lines changed: 183 additions & 54 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { z } from 'zod'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
5+
import { generateRequestId } from '@/lib/core/utils/request'
6+
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
7+
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
8+
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
9+
10+
export const dynamic = 'force-dynamic'
11+
12+
const logger = createLogger('DropboxUploadAPI')
13+
14+
/**
15+
* Escapes non-ASCII characters in JSON string for HTTP header safety.
16+
* Dropbox API requires characters 0x7F and all non-ASCII to be escaped as \uXXXX.
17+
*/
18+
function httpHeaderSafeJson(value: object): string {
19+
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => {
20+
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
21+
})
22+
}
23+
24+
const DropboxUploadSchema = z.object({
25+
accessToken: z.string().min(1, 'Access token is required'),
26+
path: z.string().min(1, 'Destination path is required'),
27+
file: FileInputSchema.optional().nullable(),
28+
// Legacy field for backwards compatibility
29+
fileContent: z.string().optional().nullable(),
30+
fileName: z.string().optional().nullable(),
31+
mode: z.enum(['add', 'overwrite']).optional().nullable(),
32+
autorename: z.boolean().optional().nullable(),
33+
mute: z.boolean().optional().nullable(),
34+
})
35+
36+
export async function POST(request: NextRequest) {
37+
const requestId = generateRequestId()
38+
39+
try {
40+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
41+
42+
if (!authResult.success) {
43+
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
44+
return NextResponse.json(
45+
{ success: false, error: authResult.error || 'Authentication required' },
46+
{ status: 401 }
47+
)
48+
}
49+
50+
logger.info(`[${requestId}] Authenticated Dropbox upload request via ${authResult.authType}`)
51+
52+
const body = await request.json()
53+
const validatedData = DropboxUploadSchema.parse(body)
54+
55+
let fileBuffer: Buffer
56+
let fileName: string
57+
58+
// Prefer UserFile input, fall back to legacy base64 string
59+
if (validatedData.file) {
60+
// Process UserFile input
61+
const userFiles = processFilesToUserFiles([validatedData.file], requestId, logger)
62+
63+
if (userFiles.length === 0) {
64+
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
65+
}
66+
67+
const userFile = userFiles[0]
68+
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
69+
70+
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
71+
fileName = userFile.name
72+
} else if (validatedData.fileContent) {
73+
// Legacy: base64 string input (backwards compatibility)
74+
logger.info(`[${requestId}] Using legacy base64 content input`)
75+
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
76+
fileName = validatedData.fileName || 'file'
77+
} else {
78+
return NextResponse.json(
79+
{ success: false, error: 'File or file content is required' },
80+
{ status: 400 }
81+
)
82+
}
83+
84+
// Determine final path
85+
let finalPath = validatedData.path
86+
if (finalPath.endsWith('/')) {
87+
finalPath = `${finalPath}${fileName}`
88+
}
89+
90+
logger.info(`[${requestId}] Uploading to Dropbox: ${finalPath} (${fileBuffer.length} bytes)`)
91+
92+
const dropboxApiArg = {
93+
path: finalPath,
94+
mode: validatedData.mode || 'add',
95+
autorename: validatedData.autorename ?? true,
96+
mute: validatedData.mute ?? false,
97+
}
98+
99+
const response = await fetch('https://content.dropboxapi.com/2/files/upload', {
100+
method: 'POST',
101+
headers: {
102+
Authorization: `Bearer ${validatedData.accessToken}`,
103+
'Content-Type': 'application/octet-stream',
104+
'Dropbox-API-Arg': httpHeaderSafeJson(dropboxApiArg),
105+
},
106+
body: fileBuffer,
107+
})
108+
109+
const data = await response.json()
110+
111+
if (!response.ok) {
112+
const errorMessage = data.error_summary || data.error?.message || 'Failed to upload file'
113+
logger.error(`[${requestId}] Dropbox API error:`, { status: response.status, data })
114+
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
115+
}
116+
117+
logger.info(`[${requestId}] File uploaded successfully to ${data.path_display}`)
118+
119+
return NextResponse.json({
120+
success: true,
121+
output: {
122+
file: data,
123+
},
124+
})
125+
} catch (error) {
126+
if (error instanceof z.ZodError) {
127+
logger.warn(`[${requestId}] Validation error:`, error.errors)
128+
return NextResponse.json(
129+
{ success: false, error: error.errors[0]?.message || 'Validation failed' },
130+
{ status: 400 }
131+
)
132+
}
133+
134+
logger.error(`[${requestId}] Unexpected error:`, error)
135+
return NextResponse.json(
136+
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
137+
{ status: 500 }
138+
)
139+
}
140+
}

apps/sim/blocks/blocks/dropbox.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,18 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
6464
id: 'uploadFile',
6565
title: 'File',
6666
type: 'file-upload',
67-
canonicalParamId: 'fileContent',
67+
canonicalParamId: 'file',
6868
placeholder: 'Upload file to send to Dropbox',
6969
mode: 'basic',
7070
multiple: false,
7171
required: true,
7272
condition: { field: 'operation', value: 'dropbox_upload' },
7373
},
7474
{
75-
id: 'fileContent',
75+
id: 'fileRef',
7676
title: 'File',
7777
type: 'short-input',
78-
canonicalParamId: 'fileContent',
78+
canonicalParamId: 'file',
7979
placeholder: 'Reference file from previous blocks',
8080
mode: 'advanced',
8181
required: true,
@@ -319,7 +319,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
319319

320320
// Normalize file input for upload operation
321321
// normalizeFileInput handles JSON stringified values from advanced mode
322-
if (params.fileContent) {
322+
if (params.file) {
323+
params.file = normalizeFileInput(params.file, { single: true })
324+
}
325+
// Legacy: also check fileContent for backwards compatibility
326+
if (params.fileContent && !params.file) {
323327
params.fileContent = normalizeFileInput(params.fileContent, { single: true })
324328
}
325329

@@ -358,7 +362,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
358362
autorename: { type: 'boolean', description: 'Auto-rename on conflict' },
359363
// Upload inputs
360364
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
361-
fileContent: { type: 'json', description: 'File reference or UserFile object' },
365+
file: { type: 'json', description: 'File to upload (UserFile object)' },
366+
fileRef: { type: 'json', description: 'File reference from previous block' },
367+
fileContent: { type: 'string', description: 'Legacy: base64 encoded file content' },
362368
fileName: { type: 'string', description: 'Optional filename' },
363369
mode: { type: 'string', description: 'Write mode: add or overwrite' },
364370
mute: { type: 'boolean', description: 'Mute notifications' },

apps/sim/tools/dropbox/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ export interface DropboxBaseParams {
7171

7272
export interface DropboxUploadParams extends DropboxBaseParams {
7373
path: string
74-
fileContent: string | UserFileLike
74+
file?: UserFileLike
75+
// Legacy field for backwards compatibility
76+
fileContent?: string
7577
fileName?: string
7678
mode?: 'add' | 'overwrite'
7779
autorename?: boolean

apps/sim/tools/dropbox/upload.ts

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
1-
import { extractBase64FromFileInput } from '@/lib/core/utils/user-file'
21
import type { DropboxUploadParams, DropboxUploadResponse } from '@/tools/dropbox/types'
32
import type { ToolConfig } from '@/tools/types'
43

5-
/**
6-
* Escapes non-ASCII characters in JSON string for HTTP header safety.
7-
* Dropbox API requires characters 0x7F and all non-ASCII to be escaped as \uXXXX.
8-
*/
9-
function httpHeaderSafeJson(value: object): string {
10-
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => {
11-
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)
12-
})
13-
}
14-
154
export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadResponse> = {
165
id: 'dropbox_upload',
176
name: 'Dropbox Upload File',
@@ -31,11 +20,18 @@ export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadRes
3120
description:
3221
'The path in Dropbox where the file should be saved (e.g., /folder/document.pdf)',
3322
},
34-
fileContent: {
35-
type: 'json',
36-
required: true,
23+
file: {
24+
type: 'file',
25+
required: false,
3726
visibility: 'user-or-llm',
38-
description: 'The file to upload (UserFile object or base64 string)',
27+
description: 'The file to upload (UserFile object)',
28+
},
29+
// Legacy field for backwards compatibility - hidden from UI
30+
fileContent: {
31+
type: 'string',
32+
required: false,
33+
visibility: 'hidden',
34+
description: 'Legacy: base64 encoded file content',
3935
},
4036
fileName: {
4137
type: 'string',
@@ -64,52 +60,37 @@ export const dropboxUploadTool: ToolConfig<DropboxUploadParams, DropboxUploadRes
6460
},
6561

6662
request: {
67-
url: 'https://content.dropboxapi.com/2/files/upload',
63+
url: '/api/tools/dropbox/upload',
6864
method: 'POST',
69-
headers: (params) => {
70-
if (!params.accessToken) {
71-
throw new Error('Missing access token for Dropbox API request')
72-
}
73-
74-
const dropboxApiArg = {
75-
path: params.path,
76-
mode: params.mode || 'add',
77-
autorename: params.autorename ?? true,
78-
mute: params.mute ?? false,
79-
}
80-
81-
return {
82-
Authorization: `Bearer ${params.accessToken}`,
83-
'Content-Type': 'application/octet-stream',
84-
'Dropbox-API-Arg': httpHeaderSafeJson(dropboxApiArg),
85-
}
86-
},
87-
body: (params) => {
88-
const base64Content = extractBase64FromFileInput(params.fileContent)
89-
if (!base64Content) {
90-
throw new Error('File Content cannot be extracted')
91-
}
92-
// Decode base64 to raw binary bytes - Dropbox expects raw binary, not base64 text
93-
return Buffer.from(base64Content, 'base64')
94-
},
65+
headers: () => ({
66+
'Content-Type': 'application/json',
67+
}),
68+
body: (params) => ({
69+
accessToken: params.accessToken,
70+
path: params.path,
71+
file: params.file,
72+
fileContent: params.fileContent,
73+
fileName: params.fileName,
74+
mode: params.mode,
75+
autorename: params.autorename,
76+
mute: params.mute,
77+
}),
9578
},
9679

97-
transformResponse: async (response, params) => {
80+
transformResponse: async (response): Promise<DropboxUploadResponse> => {
9881
const data = await response.json()
9982

100-
if (!response.ok) {
83+
if (!data.success) {
10184
return {
10285
success: false,
103-
error: data.error_summary || data.error?.message || 'Failed to upload file',
86+
error: data.error || 'Failed to upload file',
10487
output: {},
10588
}
10689
}
10790

10891
return {
10992
success: true,
110-
output: {
111-
file: data,
112-
},
93+
output: data.output,
11394
}
11495
},
11596

0 commit comments

Comments
 (0)