Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions apps/sim/app/api/files/multipart/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ vi.mock('@/lib/uploads/core/upload-token', () => ({

vi.mock('@/lib/uploads/providers/s3/client', () => ({
completeS3MultipartUpload: mockCompleteS3MultipartUpload,
initiateS3MultipartUpload: vi.fn(),
initiateS3MultipartUpload: mockInitiateS3MultipartUpload,
getS3MultipartPartUrls: vi.fn(),
abortS3MultipartUpload: vi.fn(),
}))
Expand Down Expand Up @@ -247,31 +247,38 @@ describe('POST /api/files/multipart action=initiate quota enforcement', () => {
expect(body.error).toContain('Storage limit exceeded')
})

it('does not check quota for quota-exempt contexts (og-images)', async () => {
it('allows quota-enforced contexts that pass the quota check', async () => {
const res = await makeInitiateRequest({
fileName: 'img.png',
contentType: 'image/png',
fileName: 'doc.pdf',
contentType: 'application/pdf',
fileSize: 99999,
workspaceId: 'ws-1',
context: 'og-images',
context: 'knowledge-base',
})

const response = await POST(res)
expect(mockCheckStorageQuota).not.toHaveBeenCalled()
expect(response.status).toBe(200)
expect(mockCheckStorageQuota).toHaveBeenCalledWith('user-1', 99999)
expect(mockInitiateS3MultipartUpload).toHaveBeenCalled()
})

it('rejects logs context — not allowed via the multipart endpoint', async () => {
const res = await makeInitiateRequest({
fileName: 'exec.log',
contentType: 'text/plain',
fileSize: 1000,
workspaceId: 'ws-1',
context: 'logs',
})
it.each(['og-images', 'profile-pictures', 'workspace-logos', 'logs'])(
'rejects quota-exempt context %s — not allowed via the multipart endpoint',
async (context) => {
const res = await makeInitiateRequest({
fileName: 'asset.png',
contentType: 'image/png',
fileSize: 100 * 1024 * 1024 * 1024,
workspaceId: 'ws-1',
context,
})

const response = await POST(res)
expect(response.status).toBe(400)
const body = await response.json()
expect(body.error).toMatch(/invalid storage context/i)
})
const response = await POST(res)
expect(response.status).toBe(400)
const body = await response.json()
expect(body.error).toMatch(/invalid storage context/i)
expect(mockCheckStorageQuota).not.toHaveBeenCalled()
expect(mockInitiateS3MultipartUpload).not.toHaveBeenCalled()
}
)
})
14 changes: 10 additions & 4 deletions apps/sim/app/api/files/multipart/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('MultipartUploadAPI')

/**
* Contexts the multipart endpoint accepts. The quota-exempt public-asset
* contexts (`profile-pictures`, `workspace-logos`, `og-images`) and the
* system-internal `logs` context are deliberately excluded: their uploads are
* small images capped far below the multipart threshold and routed through the
* presigned endpoint, so they have no large-file flow here. Accepting them would
* only expose a path that bypasses the per-user storage quota, since every
* context in this set is quota-enforced below.
*/
const ALLOWED_UPLOAD_CONTEXTS = new Set<StorageContext>([
'knowledge-base',
'chat',
'copilot',
'mothership',
'execution',
'workspace',
'profile-pictures',
'og-images',
'workspace-logos',
])

/**
Expand Down Expand Up @@ -159,7 +165,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

const config = getStorageConfig(storageContext)

if (!QUOTA_EXEMPT_STORAGE_CONTEXTS.has(context as StorageContext)) {
if (!QUOTA_EXEMPT_STORAGE_CONTEXTS.has(storageContext)) {
const { checkStorageQuota } = await import('@/lib/billing/storage')
const quotaCheck = await checkStorageQuota(userId, fileSize ?? 0)
if (!quotaCheck.allowed) {
Expand Down
8 changes: 5 additions & 3 deletions apps/sim/lib/uploads/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ export type StorageContext =
* metadata assets (`profile-pictures`, `workspace-logos`, `og-images`). All
* other contexts are user-driven uploads and must pass quota validation.
*
* Note: `logs` is excluded from `ALLOWED_UPLOAD_CONTEXTS` in the multipart
* endpoint, so it is unreachable there. The exemption applies to non-multipart
* (single-part) upload paths used by the execution logging pipeline.
* Note: every quota-exempt context is excluded from `ALLOWED_UPLOAD_CONTEXTS`
* in the multipart endpoint, so none are reachable there — the exemption applies
* only to the single-part upload paths (presigned/FormData) those small assets
* actually use. The multipart endpoint therefore only ever serves
* quota-enforced contexts.
*/
export const QUOTA_EXEMPT_STORAGE_CONTEXTS = new Set<StorageContext>([
'profile-pictures',
Expand Down
Loading