Skip to content

Commit 18bf362

Browse files
committed
fix(files): resolve every generated-document source type, not four of five
The archive keyed off a locally enumerated set of source content types that omitted text/x-pdflibjs — the isolated-vm PDF generator. Those files failed the check and streamed their generator source under a .pdf name, which is the exact corruption this change exists to fix. A canonical five-member set already existed in the file viewer; enumerating a fourth copy was the mistake, not the missing string. The set moves to file-utils beside the other file-type predicates, the viewer imports it, and the route asks isGeneratedDocumentSourceType rather than carrying its own list. A parameterized test pins all five, so adding a generator without extending the set fails.
1 parent 9cff2b7 commit 18bf362

4 files changed

Lines changed: 44 additions & 22 deletions

File tree

apps/sim/app/api/workspaces/[id]/files/download/route.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,27 @@ describe('workspace files download route', () => {
248248
expect(body.error).not.toContain('second.docx')
249249
})
250250

251+
it.each([
252+
['text/x-docxjs', 'report.docx'],
253+
['text/x-pptxgenjs', 'deck.pptx'],
254+
['text/x-pdflibjs', 'isolated.pdf'],
255+
['text/x-python-pdf', 'sandboxed.pdf'],
256+
['text/x-python-xlsx', 'sheet.xlsx'],
257+
])('resolves %s rather than streaming its source', async (type, name) => {
258+
// Both PDF generators must be covered: the isolated-vm path stores pdf-lib JS and
259+
// the E2B path stores Python, and either one streamed raw is the corruption bug.
260+
mockListWorkspaceFiles.mockResolvedValue([{ ...workspaceFile('f1', name), type }])
261+
mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
262+
buffer: Buffer.from('PKrendered'),
263+
contentType: 'application/octet-stream',
264+
})
265+
266+
await zipFrom(await GET(requestFor('fileIds=f1'), context))
267+
268+
expect(mockFetchServableWorkspaceFileBuffer).toHaveBeenCalledTimes(1)
269+
expect(mockDownloadFileStream).not.toHaveBeenCalled()
270+
})
271+
251272
it('streams an uploaded office file rather than resolving it', async () => {
252273
// A real upload serves exactly its stored bytes, so it must not take the buffered
253274
// path — otherwise a selection of large decks is held in memory for nothing.

apps/sim/app/api/workspaces/[id]/files/download/route.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
77
import { parseRequest } from '@/lib/api/server'
88
import { getSession } from '@/lib/auth'
9-
import {
10-
DOCXJS_SOURCE_MIME,
11-
PPTXGENJS_SOURCE_MIME,
12-
PYTHON_PDF_SOURCE_MIME,
13-
PYTHON_XLSX_SOURCE_MIME,
14-
} from '@/lib/copilot/tools/server/files/doc-compile'
159
import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream'
1610
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1711
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -26,6 +20,7 @@ import {
2620
import { downloadFileStream } from '@/lib/uploads/core/storage-service'
2721
import {
2822
formatFileSize,
23+
isGeneratedDocumentSourceType,
2924
isRenderableDocumentName,
3025
MAX_RENDERED_DOCUMENT_BYTES,
3126
} from '@/lib/uploads/utils/file-utils'
@@ -37,14 +32,6 @@ const logger = createLogger('WorkspaceFilesDownloadAPI')
3732
const MAX_ZIP_DOWNLOAD_FILES = 100
3833
const MAX_ZIP_DOWNLOAD_BYTES = 250 * 1024 * 1024
3934

40-
/** Content types under which a generated document's *source* is stored. */
41-
const GENERATED_SOURCE_TYPES = new Set<string>([
42-
DOCXJS_SOURCE_MIME,
43-
PPTXGENJS_SOURCE_MIME,
44-
PYTHON_PDF_SOURCE_MIME,
45-
PYTHON_XLSX_SOURCE_MIME,
46-
])
47-
4835
/**
4936
* Whether this entry's stored bytes are a generation source that has to be resolved
5037
* before it can go in the archive. An ordinary uploaded `.docx` serves exactly what is
@@ -55,7 +42,7 @@ const GENERATED_SOURCE_TYPES = new Set<string>([
5542
* a document name.
5643
*/
5744
function needsRendering(file: WorkspaceFileRecord): boolean {
58-
return file.type ? GENERATED_SOURCE_TYPES.has(file.type) : isRenderableDocumentName(file.name)
45+
return file.type ? isGeneratedDocumentSourceType(file.type) : isRenderableDocumentName(file.name)
5946
}
6047

6148
/**

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
44
import { toast } from '@sim/emcn'
55
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
6+
import { GENERATED_DOCUMENT_SOURCE_TYPES } from '@/lib/uploads/utils/file-utils'
67
import {
78
useUpdateWorkspaceFileContent,
89
useWorkspaceFileContent,
@@ -20,13 +21,7 @@ import {
2021
* editable text is the source program, not the compiled artifact. The serve route
2122
* returns that source only when asked for the raw representation.
2223
*/
23-
const GENERATED_SOURCE_FILE_TYPES = new Set([
24-
'text/x-pptxgenjs',
25-
'text/x-docxjs',
26-
'text/x-pdflibjs',
27-
'text/x-python-pdf',
28-
'text/x-python-xlsx',
29-
])
24+
const GENERATED_SOURCE_FILE_TYPES = GENERATED_DOCUMENT_SOURCE_TYPES
3025

3126
/**
3227
* Poll cadence for the content query while the post-stream reconcile waits for a fetch showing the

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,25 @@ export function getFileExtension(filename: string): string {
217217
*/
218218
const RENDERABLE_DOCUMENT_EXTENSIONS = new Set(['pdf', 'docx', 'pptx', 'xlsx'])
219219

220+
/**
221+
* Content types under which a generated document's *generation source* is stored. A
222+
* file carrying one of these renders to something other than its stored bytes, so any
223+
* surface that hands out the file itself has to resolve it first. Both PDF generators
224+
* are here: the E2B path stores Python, the isolated-vm path stores pdf-lib JS.
225+
*/
226+
export const GENERATED_DOCUMENT_SOURCE_TYPES = new Set<string>([
227+
'text/x-docxjs',
228+
'text/x-pptxgenjs',
229+
'text/x-pdflibjs',
230+
'text/x-python-pdf',
231+
'text/x-python-xlsx',
232+
])
233+
234+
/** True when the stored bytes for `contentType` are a generation source. */
235+
export function isGeneratedDocumentSourceType(contentType: string | undefined | null): boolean {
236+
return contentType ? GENERATED_DOCUMENT_SOURCE_TYPES.has(contentType) : false
237+
}
238+
220239
/**
221240
* Ceiling on a single rendered generated document. A generator source is text and is
222241
* orders of magnitude smaller than the document it produces, so the declared size is no

0 commit comments

Comments
 (0)