Skip to content

Commit 8c2750d

Browse files
fix(file): resolve basic-picker files for manage sharing
The basic file-upload picker stores a workspace file as { name, path, key, size, type } with no canonical id, so manage_sharing failed those picks with 'Could not determine the file to share'. The block now passes the picked object as fileInput when it lacks an id, and the route resolves the canonical id from the storage key via getFileMetadataByKey. Contract accepts fileId OR fileInput (mirroring read/get content).
1 parent 80bb8ed commit 8c2750d

5 files changed

Lines changed: 105 additions & 38 deletions

File tree

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
updateWorkspaceFileContent,
2929
uploadWorkspaceFile,
3030
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
31+
import { getFileMetadataByKey } from '@/lib/uploads/server/metadata'
3132
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
3233
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
3334
import { performMoveWorkspaceFileItems } from '@/lib/workspace-files/orchestration'
@@ -577,7 +578,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
577578
}
578579

579580
case 'manage_sharing': {
580-
const { fileId, isActive, authType, password, allowedEmails } = body
581+
const { fileId, fileInput, isActive, authType, password, allowedEmails } = body
581582

582583
// Check permission before probing file existence so a read-only caller
583584
// can't distinguish 404 from 403 as a file-existence side channel.
@@ -591,10 +592,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
591592
)
592593
}
593594

594-
const file = await getWorkspaceFile(workspaceId, fileId)
595+
// Resolve the canonical file id. The basic file picker provides an object
596+
// with a storage `key` but no id, so map the key to the workspace file row.
597+
let resolvedFileId = typeof fileId === 'string' ? fileId : undefined
598+
if (!resolvedFileId && fileInput) {
599+
const single = Array.isArray(fileInput) ? fileInput[0] : fileInput
600+
if (single && typeof single === 'object') {
601+
const record = single as Record<string, unknown>
602+
if (typeof record.id === 'string' && record.id) resolvedFileId = record.id
603+
else if (typeof record.fileId === 'string' && record.fileId)
604+
resolvedFileId = record.fileId
605+
else if (typeof record.key === 'string' && record.key) {
606+
const meta = await getFileMetadataByKey(record.key, 'workspace')
607+
resolvedFileId = meta?.id
608+
}
609+
}
610+
}
611+
if (!resolvedFileId) {
612+
return NextResponse.json(
613+
{ success: false, error: 'A valid file is required to manage sharing' },
614+
{ status: 400 }
615+
)
616+
}
617+
618+
const file = await getWorkspaceFile(workspaceId, resolvedFileId)
595619
if (!file) {
596620
return NextResponse.json(
597-
{ success: false, error: `File not found: "${fileId}"` },
621+
{ success: false, error: `File not found: "${resolvedFileId}"` },
598622
{ status: 404 }
599623
)
600624
}
@@ -605,7 +629,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
605629
// Resolve the auth type the same way upsertFileShare will (falling back
606630
// to the existing share's type) so the policy gate can't be bypassed by
607631
// re-enabling a pre-existing restricted share without an explicit authType.
608-
const existingShare = await getShareForResource('file', fileId)
632+
const existingShare = await getShareForResource('file', resolvedFileId)
609633
const resolvedAuthType = authType ?? existingShare?.authType ?? 'public'
610634
try {
611635
await validatePublicFileSharing(userId, workspaceId, resolvedAuthType)
@@ -619,7 +643,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
619643

620644
const share = await upsertFileShare({
621645
workspaceId,
622-
fileId,
646+
fileId: resolvedFileId,
623647
userId,
624648
isActive,
625649
authType,
@@ -632,13 +656,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
632656
actorId: userId,
633657
action: isActive ? AuditAction.FILE_SHARED : AuditAction.FILE_SHARE_DISABLED,
634658
resourceType: AuditResourceType.FILE,
635-
resourceId: fileId,
659+
resourceId: resolvedFileId,
636660
resourceName: file.name,
637661
description: `${isActive ? 'Enabled' : 'Disabled'} public share for "${file.name}"`,
638662
request,
639663
})
640664

641-
logger.info('File sharing updated', { fileId, isActive, authType: share.authType })
665+
logger.info('File sharing updated', {
666+
fileId: resolvedFileId,
667+
isActive,
668+
authType: share.authType,
669+
})
642670

643671
// A disabled link doesn't resolve, so don't hand back a dead URL.
644672
const responseShare = share.isActive ? share : { ...share, url: '' }

apps/sim/blocks/blocks/file.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,28 @@ describe('FileV5Block', () => {
200200
})
201201
})
202202

203+
it('passes a picker file object without an id through as fileInput for manage sharing', () => {
204+
const picked = {
205+
name: 'report.pdf',
206+
key: 'workspace/workspace-1/123-abc-report.pdf',
207+
path: '/api/files/serve/workspace%2Fworkspace-1%2F123-abc-report.pdf?context=workspace',
208+
size: 10,
209+
type: 'application/pdf',
210+
}
211+
expect(
212+
buildParams({
213+
operation: 'file_manage_sharing',
214+
shareInput: [picked],
215+
shareVisibility: 'public',
216+
_context: { workspaceId: 'workspace-1' },
217+
})
218+
).toMatchObject({
219+
fileInput: picked,
220+
isActive: true,
221+
authType: 'public',
222+
})
223+
})
224+
203225
it('throws when no file is provided for manage sharing', () => {
204226
expect(() => buildParams({ operation: 'file_manage_sharing' })).toThrow(
205227
'File is required to manage sharing'

apps/sim/blocks/blocks/file.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,22 +1208,6 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
12081208
throw new Error('File is required to manage sharing')
12091209
}
12101210

1211-
let fileId: string
1212-
const fileIds = parseReadFileIds(shareInput)
1213-
if (fileIds) {
1214-
if (Array.isArray(fileIds) && fileIds.length > 1) {
1215-
throw new Error('Manage Sharing accepts a single file at a time')
1216-
}
1217-
fileId = Array.isArray(fileIds) ? fileIds[0] : fileIds
1218-
} else {
1219-
const normalized = normalizeFileInput(shareInput, { single: true })
1220-
const file = normalized as Record<string, unknown> | null
1221-
fileId = (file?.id as string) ?? ''
1222-
}
1223-
if (!fileId) {
1224-
throw new Error('Could not determine the file to share')
1225-
}
1226-
12271211
const allowedEmails =
12281212
typeof params.shareAllowedEmails === 'string'
12291213
? params.shareAllowedEmails
@@ -1234,16 +1218,36 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
12341218

12351219
const visibility = (params.shareVisibility as string) || 'public'
12361220
const isActive = visibility !== 'private'
1237-
1238-
return {
1239-
fileId,
1221+
const shareParams = {
12401222
isActive,
12411223
// When disabling, leave authType unset so the stored access mode is preserved.
12421224
authType: isActive ? visibility : undefined,
12431225
password: params.sharePassword,
12441226
allowedEmails,
12451227
workspaceId: params._context?.workspaceId,
12461228
}
1229+
1230+
// Canonical IDs (advanced mode or upstream references) resolve directly.
1231+
const fileIds = parseReadFileIds(shareInput)
1232+
if (fileIds) {
1233+
if (Array.isArray(fileIds) && fileIds.length > 1) {
1234+
throw new Error('Manage Sharing accepts a single file at a time')
1235+
}
1236+
return { fileId: Array.isArray(fileIds) ? fileIds[0] : fileIds, ...shareParams }
1237+
}
1238+
1239+
// The basic picker yields a file object; it carries an id only sometimes,
1240+
// so prefer the id when present and otherwise pass the object for the
1241+
// route to resolve via its storage key.
1242+
const normalized = normalizeFileInput(shareInput, { single: true })
1243+
const file = normalized as Record<string, unknown> | null
1244+
if (!file) {
1245+
throw new Error('Could not determine the file to share')
1246+
}
1247+
if (typeof file.id === 'string' && file.id) {
1248+
return { fileId: file.id, ...shareParams }
1249+
}
1250+
return { fileInput: normalized, ...shareParams }
12471251
}
12481252

12491253
if (operation === 'file_fetch') {

apps/sim/lib/api/contracts/tools/file.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,20 @@ export const fileManageMoveBodySchema = z.object({
4343

4444
export type FileManageMoveBody = z.input<typeof fileManageMoveBodySchema>
4545

46-
export const fileManageSharingBodySchema = z.object({
47-
operation: z.literal('manage_sharing'),
48-
workspaceId: z.string().min(1).optional(),
49-
fileId: z.string().min(1, 'fileId is required for manage_sharing operation'),
50-
isActive: z.boolean({ error: 'isActive is required for manage_sharing operation' }),
51-
authType: shareAuthTypeSchema.optional(),
52-
password: z.string().min(1).max(1024).optional(),
53-
allowedEmails: z.array(z.string().min(1)).max(200).optional(),
54-
})
46+
export const fileManageSharingBodySchema = z
47+
.object({
48+
operation: z.literal('manage_sharing'),
49+
workspaceId: z.string().min(1).optional(),
50+
fileId: z.string().min(1).optional(),
51+
fileInput: z.unknown().optional(),
52+
isActive: z.boolean({ error: 'isActive is required for manage_sharing operation' }),
53+
authType: shareAuthTypeSchema.optional(),
54+
password: z.string().min(1).max(1024).optional(),
55+
allowedEmails: z.array(z.string().min(1)).max(200).optional(),
56+
})
57+
.refine((data) => data.fileId !== undefined || data.fileInput !== undefined, {
58+
message: 'Either fileId or fileInput is required for manage_sharing operation',
59+
})
5560

5661
export type FileManageSharingBody = z.input<typeof fileManageSharingBodySchema>
5762

apps/sim/tools/file/manage-sharing.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { ShareAuthType } from '@/lib/api/contracts/public-shares'
22
import type { ToolConfig, ToolResponse, WorkflowToolExecutionContext } from '@/tools/types'
33

44
interface FileManageSharingParams {
5-
fileId: string
5+
fileId?: string
6+
fileInput?: unknown
67
isActive: boolean
78
authType?: ShareAuthType
89
password?: string
@@ -21,9 +22,15 @@ export const fileManageSharingTool: ToolConfig<FileManageSharingParams, ToolResp
2122
params: {
2223
fileId: {
2324
type: 'string',
24-
required: true,
25+
required: false,
2526
visibility: 'user-or-llm',
26-
description: 'The ID of the workspace file to update sharing for.',
27+
description: 'Canonical ID of the workspace file to update sharing for.',
28+
},
29+
fileInput: {
30+
type: 'file',
31+
required: false,
32+
visibility: 'user-only',
33+
description: 'Selected workspace file object (from the file picker).',
2734
},
2835
isActive: {
2936
type: 'boolean',
@@ -60,6 +67,7 @@ export const fileManageSharingTool: ToolConfig<FileManageSharingParams, ToolResp
6067
body: (params) => ({
6168
operation: 'manage_sharing',
6269
fileId: params.fileId,
70+
fileInput: params.fileInput,
6371
isActive: params.isActive,
6472
authType: params.authType,
6573
password: params.password,

0 commit comments

Comments
 (0)