Skip to content

Commit 73c73ff

Browse files
fix(kb): canonicalize knowledge-base upload keys (#5096)
* fix(kb): canonicalize knowledge-base upload keys * fix tests
1 parent d14bc78 commit 73c73ff

6 files changed

Lines changed: 59 additions & 10 deletions

File tree

apps/sim/app/api/files/presigned/route.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ function setupFileApiMocks(
158158

159159
storageServiceMockFns.mockHasCloudStorage.mockReturnValue(cloudEnabled)
160160
storageServiceMockFns.mockGeneratePresignedUploadUrl.mockImplementation(
161-
async (opts: { fileName: string; context: string }) => {
161+
async (opts: { fileName: string; context: string; customKey?: string }) => {
162162
const timestamp = Date.now()
163163
const safeFileName = opts.fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
164-
const key = `${opts.context}/${timestamp}-ik3a6w4-${safeFileName}`
164+
const key = opts.customKey ?? `${opts.context}/${timestamp}-ik3a6w4-${safeFileName}`
165165
return {
166166
url: 'https://example.com/presigned-url',
167167
key,
@@ -369,7 +369,7 @@ describe('/api/files/presigned', () => {
369369
const data = await response.json()
370370

371371
expect(response.status).toBe(200)
372-
expect(data.fileInfo.key).toMatch(/^knowledge-base\/.*knowledge-doc\.pdf$/)
372+
expect(data.fileInfo.key).toMatch(/^kb\/.*knowledge-doc\.pdf$/)
373373
expect(data.directUploadSupported).toBe(true)
374374
})
375375

apps/sim/app/api/files/presigned/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CopilotFiles } from '@/lib/uploads'
99
import type { StorageContext } from '@/lib/uploads/config'
1010
import { USE_BLOB_STORAGE } from '@/lib/uploads/config'
1111
import { generateExecutionFileKey } from '@/lib/uploads/contexts/execution/utils'
12+
import { generateKnowledgeBaseFileKey } from '@/lib/uploads/contexts/knowledge-base/knowledge-base-file-manager'
1213
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1314
import { generatePresignedUploadUrl, hasCloudStorage } from '@/lib/uploads/core/storage-service'
1415
import { insertFileMetadata, recordKnowledgeBaseFileOwnership } from '@/lib/uploads/server/metadata'
@@ -268,12 +269,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
268269
)
269270
}
270271

272+
const customKey = generateKnowledgeBaseFileKey(fileName)
271273
presignedUrlResponse = await generatePresignedUploadUrl({
272274
fileName,
273275
contentType,
274276
fileSize,
275277
context: 'knowledge-base',
276278
userId: sessionUserId,
279+
customKey,
277280
expirationSeconds: 3600,
278281
metadata: { workspaceId },
279282
})

apps/sim/app/api/files/upload/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
2020
import { captureServerEvent } from '@/lib/posthog/server'
2121
import type { StorageContext } from '@/lib/uploads/config'
22+
import { generateKnowledgeBaseFileKey } from '@/lib/uploads/contexts/knowledge-base/knowledge-base-file-manager'
2223
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
2324
import { MAX_WORKSPACE_FORMDATA_FILE_SIZE } from '@/lib/uploads/shared/types'
2425
import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
@@ -155,9 +156,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
155156

156157
logger.info(`Uploading knowledge-base file: ${originalName}`)
157158

158-
const timestamp = Date.now()
159-
const safeFileName = sanitizeFileName(originalName)
160-
const storageKey = `kb/${timestamp}-${safeFileName}`
159+
const storageKey = generateKnowledgeBaseFileKey(originalName)
161160

162161
const metadata: Record<string, string> = {
163162
originalName: originalName,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { randomBytes } from 'crypto'
2+
import { sanitizeFileName } from '@/executor/constants'
3+
4+
/**
5+
* Generate a canonical knowledge-base storage key.
6+
*
7+
* Direct/presigned uploads previously used the generic `${context}/...` key
8+
* shape (`knowledge-base/...`). New KB uploads should use the same `kb/...`
9+
* prefix as server-side uploads so key-derived context inference is consistent.
10+
*/
11+
export function generateKnowledgeBaseFileKey(fileName: string): string {
12+
const timestamp = Date.now()
13+
const random = randomBytes(8).toString('hex')
14+
const safeFileName = sanitizeFileName(fileName)
15+
return `kb/${timestamp}-${random}-${safeFileName}`
16+
}

apps/sim/lib/uploads/utils/file-utils.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { createLogger } from '@sim/logger'
55
import { describe, expect, it } from 'vitest'
66
import {
7+
inferContextFromKey,
78
isAbortError,
89
isInternalFileUrl,
910
isNetworkError,
@@ -47,6 +48,32 @@ describe('isInternalFileUrl', () => {
4748
})
4849
})
4950

51+
describe('inferContextFromKey', () => {
52+
it('maps both kb/ and knowledge-base/ prefixes to knowledge-base', () => {
53+
expect(inferContextFromKey('kb/1700000000000-doc.pdf')).toBe('knowledge-base')
54+
// Direct/presigned uploads key as `${context}/...`, i.e. `knowledge-base/...`
55+
expect(inferContextFromKey('knowledge-base/1781612506186-b2442e0dc045cb6c-doc.txt')).toBe(
56+
'knowledge-base'
57+
)
58+
})
59+
60+
it('maps the remaining context prefixes', () => {
61+
expect(inferContextFromKey('chat/x')).toBe('chat')
62+
expect(inferContextFromKey('copilot/x')).toBe('copilot')
63+
expect(inferContextFromKey('execution/ws/wf/ex/x')).toBe('execution')
64+
expect(inferContextFromKey('workspace/ws/x')).toBe('workspace')
65+
expect(inferContextFromKey('profile-pictures/x')).toBe('profile-pictures')
66+
expect(inferContextFromKey('og-images/x')).toBe('og-images')
67+
expect(inferContextFromKey('workspace-logos/x')).toBe('workspace-logos')
68+
expect(inferContextFromKey('logs/x')).toBe('logs')
69+
})
70+
71+
it('throws for empty or unrecognized keys', () => {
72+
expect(() => inferContextFromKey('')).toThrow()
73+
expect(() => inferContextFromKey('mystery/x')).toThrow()
74+
})
75+
})
76+
5077
describe('isAbortError', () => {
5178
it('returns true for AbortError-named errors', () => {
5279
const err = new Error('aborted')

apps/sim/lib/uploads/utils/file-utils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,15 +565,19 @@ export function isInternalFileUrl(fileUrl: string): boolean {
565565
}
566566

567567
/**
568-
* Infer storage context from file key using explicit prefixes
569-
* All files must use prefixed keys
568+
* Infer storage context from a file key using its prefix.
569+
*
570+
* All stored files use prefixed keys. Knowledge-base objects carry one of two
571+
* prefixes: `kb/` (server-side uploads) or `knowledge-base/` (direct/presigned
572+
* uploads, whose default key is `${context}/...`). Both map to the same
573+
* `knowledge-base` context.
570574
*/
571575
export function inferContextFromKey(key: string): StorageContext {
572576
if (!key) {
573577
throw new Error('Cannot infer context from empty key')
574578
}
575579

576-
if (key.startsWith('kb/')) return 'knowledge-base'
580+
if (key.startsWith('kb/') || key.startsWith('knowledge-base/')) return 'knowledge-base'
577581
if (key.startsWith('chat/')) return 'chat'
578582
if (key.startsWith('copilot/')) return 'copilot'
579583
if (key.startsWith('execution/')) return 'execution'
@@ -584,7 +588,7 @@ export function inferContextFromKey(key: string): StorageContext {
584588
if (key.startsWith('logs/')) return 'logs'
585589

586590
throw new Error(
587-
`File key must start with a context prefix (kb/, chat/, copilot/, execution/, workspace/, profile-pictures/, og-images/, workspace-logos/, or logs/). Got: ${key}`
591+
`File key must start with a context prefix (kb/, knowledge-base/, chat/, copilot/, execution/, workspace/, profile-pictures/, og-images/, workspace-logos/, or logs/). Got: ${key}`
588592
)
589593
}
590594

0 commit comments

Comments
 (0)