diff --git a/apps/sim/app/api/files/serve/[...path]/route.test.ts b/apps/sim/app/api/files/serve/[...path]/route.test.ts index d785e481f9c..99323a9ce90 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -571,17 +571,53 @@ describe('File Serve API Route', () => { describe('outside the workspace-file model', () => { const key = 'workspace/ws/1790000000000-0123456789abcdef-clip.mp4' - function serveAttachment(headers: Record = {}) { + function serveAttachment(headers: Record = {}, objectKey = key) { mockIsUsingCloudStorage.mockReturnValue(true) storageServiceMockFns.mockDownloadFileStream.mockImplementation(async () => Readable.from([Buffer.from('attachment')]) ) return GET( - new NextRequest(`http://localhost:3000/api/files/serve/${key}`, { headers }), - createRouteContext({ path: key.split('/') }) + new NextRequest(`http://localhost:3000/api/files/serve/${objectKey}`, { headers }), + createRouteContext({ path: objectKey.split('/') }) ) } + describe('a document name', () => { + const documentKey = 'workspace/ws/1790000000000-0123456789abcdef-scan.pdf' + + beforeEach(() => { + storageServiceMockFns.mockHeadObject.mockResolvedValue({ size: 10, version: '"v1"' }) + storageServiceMockFns.mockDownloadFile.mockResolvedValue(Buffer.from('rendered')) + }) + + it('streams when its file record says it is a finished upload', async () => { + uploadsMetadataMockFns.mockGetFileMetadataByKey.mockResolvedValue({ + key: documentKey, + contentType: 'application/pdf', + }) + + const response = await serveAttachment({}, documentKey) + + expect(response.status).toBe(200) + expect(storageServiceMockFns.mockDownloadFileStream).toHaveBeenCalled() + expect(storageServiceMockFns.mockDownloadFile).not.toHaveBeenCalled() + }) + + it.each([ + ['its record says it is a generation source', { contentType: 'text/x-pdflibjs' }], + ['it has no file record', null], + ])('stays buffered when %s', async (_label, record) => { + uploadsMetadataMockFns.mockGetFileMetadataByKey.mockResolvedValue( + record && { key: documentKey, ...record } + ) + + await serveAttachment({}, documentKey) + + expect(storageServiceMockFns.mockDownloadFileStream).not.toHaveBeenCalled() + expect(storageServiceMockFns.mockDownloadFile).toHaveBeenCalled() + }) + }) + it('streams a range of an attachment sized by storage, never buffering it', async () => { storageServiceMockFns.mockHeadObject.mockResolvedValue({ size: 500_000_000, diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index 88fcccac774..df007d91f25 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -28,11 +28,11 @@ import { workspaceFileCacheControl, } from '@/lib/uploads/server/delivery' import { resolveServableImageBytes } from '@/lib/uploads/server/image-derivative' -import { resolveStoredFileContext } from '@/lib/uploads/server/metadata' +import { getFileMetadataByKey, resolveStoredFileContext } from '@/lib/uploads/server/metadata' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { inferContextFromKey, - needsRenderedArtifact, + isRenderableDocumentName, resolveEffectiveMimeType, } from '@/lib/uploads/utils/file' import { internalWorkspaceFileServeAuth } from '@/lib/workspace-files/api' @@ -40,8 +40,10 @@ import { fetchAuthorizedServableWorkspaceFileBuffer } from '@/lib/workspace-file import { finishFileDelivery } from '@/lib/workspace-files/application/finish-file-delivery' import { readWorkspaceFileRecordByKey } from '@/lib/workspace-files/application/read-workspace-file-record-by-key' import { streamAuthorizedWorkspaceFile } from '@/lib/workspace-files/application/stream-authorized-workspace-file' -import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { isSimPageSource } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { renderSimPageDocumentWithAssets } from '@/lib/workspace-files/page-document.server' +import { servesStoredBytes } from '@/lib/workspace-files/stored-bytes' import { type KnowledgeFileAccess, verifyFileAccess } from '@/app/api/files/authorization' import { createConditionalFileResponse, @@ -203,10 +205,10 @@ async function resolveTransformedBytes(params: { * Whether a request is answered with the stored object byte-for-byte, so its bytes can stream from * storage instead of being read whole and capped at the transfer ceiling. * - * Every other answer is produced from the bytes and stays buffered: a Sim page renders, a generated - * document compiles, and a `preview` may be a HEIC transcode — sniffed from the content, so no name - * or type rules it out. What streams is exactly what {@link resolveServableBytes} and the - * workspace resolver would have passed through unchanged. + * Every other answer is produced from the bytes and stays buffered: see {@link servesStoredBytes}, + * plus a `preview`, which may be a HEIC transcode — sniffed from the content, so no name or type + * rules it out. What streams is exactly what {@link resolveServableBytes} and the workspace + * resolver would have passed through unchanged. */ function servesStoredObject( filename: string, @@ -215,12 +217,7 @@ function servesStoredObject( ): boolean { if (options.raw) return true if (options.preview) return false - if (recordedType === SIM_PAGE_CONTENT_TYPE || filename.toLowerCase().endsWith('.html')) { - return false - } - // A recorded type tells a finished upload from a generation source; without one, every - // document name may be a source. - return !needsRenderedArtifact(recordedType, filename) + return servesStoredBytes(recordedType, filename) } /** @@ -231,11 +228,26 @@ function storedObjectContentType(filename: string, options: ServeOptions): strin return options.raw ? getContentType(filename) : resolveEffectiveMimeType(undefined, filename) } +/** + * The type recorded for an object outside the workspace-file model, read only where it decides the + * answer: a document name may be a finished upload or a generation source, and the object's file + * record, where it has one, says which. An object with no record is judged by its name. + */ +async function recordedDocumentType( + key: string, + context: StorageContext, + filename: string, + options: ServeOptions +): Promise { + if (options.raw || options.preview || !isRenderableDocumentName(filename)) return undefined + return (await getFileMetadataByKey(key, context))?.contentType +} + /** * Streams a stored object outside the workspace-file model (execution outputs, chat and copilot * attachments, knowledge documents) when the request is answered with it as-is, or returns null - * for the buffered path. The object is its only record, so storage is asked for its length and - * version, and the version is what proves a revalidation still holds. + * for the buffered path. Storage is asked for the object's length and version, and the version is + * what proves a revalidation still holds. */ async function streamStoredObject(params: { key: string @@ -245,7 +257,8 @@ async function streamStoredObject(params: { request: NextRequest }): Promise { const { key, context, filename, options } = params - if (!servesStoredObject(filename, undefined, options)) return null + const recordedType = await recordedDocumentType(key, context, filename, options) + if (!servesStoredObject(filename, recordedType, options)) return null const stored = await headObject(key, context) if (!stored) throw new FileNotFoundError(`File not found: ${key}`) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx index f1b6be2eee9..29954e3d954 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx @@ -27,7 +27,8 @@ import { import type { FileDownloadSource } from '@/lib/uploads/client/download' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { getFileExtension } from '@/lib/uploads/utils/file' -import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { isSimPageSource } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { EditorContextMenu } from '@/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu' import { FileSaveConflict } from '@/app/workspace/[workspaceId]/files/components/file-viewer/file-save-conflict' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer' diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 1932875c56c..76aad2ac614 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -48,7 +48,7 @@ import { isFileBrowserSize, isFileBrowserType, } from '@/lib/workspace-files/browser' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { FindBar } from '@/app/workspace/[workspaceId]/components/find-bar/find-bar' import { useFindShortcut } from '@/app/workspace/[workspaceId]/components/find-bar/use-find-shortcut' import type { MoveOptionNode } from '@/app/workspace/[workspaceId]/components/folders' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index c4f55ca3292..790062e140f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -11,7 +11,7 @@ import { } from '@/lib/mothership/resources/types' import type { FileDownloadSource } from '@/lib/uploads/client/download' import { getFileExtension } from '@/lib/uploads/utils/file' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { isCsvStreamOnly, diff --git a/apps/sim/lib/mothership/tools/server/files/edit-content.ts b/apps/sim/lib/mothership/tools/server/files/edit-content.ts index a4e4559c6bc..9e33cd1225d 100644 --- a/apps/sim/lib/mothership/tools/server/files/edit-content.ts +++ b/apps/sim/lib/mothership/tools/server/files/edit-content.ts @@ -21,8 +21,8 @@ import { HAND_WRITTEN_PAGE_MESSAGE, isHandWrittenCompiledPage, isSimPageSource, - SIM_PAGE_CONTENT_TYPE, } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { buildEmbeddedImageRefWarning } from './embedded-image-refs' import { waitForLatestFileIntent } from './intent-store' import { compileDocForWrite, getDocumentFormatInfo, inferContentType } from './workspace-file' diff --git a/apps/sim/lib/projects/files/application/artifacts.ts b/apps/sim/lib/projects/files/application/artifacts.ts index 7be5134b2a8..00dc93648dd 100644 --- a/apps/sim/lib/projects/files/application/artifacts.ts +++ b/apps/sim/lib/projects/files/application/artifacts.ts @@ -56,11 +56,8 @@ import { textExtractionUnsupportedError, } from '@/lib/workspace-files/application/read-workspace-file-text' import { fileOwnerCondition } from '@/lib/workspace-files/ownership-query' -import { - isSimPageSource, - ProjectPageForeignEmbedError, - SIM_PAGE_CONTENT_TYPE, -} from '@/lib/workspace-files/page-compile' +import { isSimPageSource, ProjectPageForeignEmbedError } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { renderSimPageDocument } from '@/lib/workspace-files/page-document' import { collectSimPageFileReferences, diff --git a/apps/sim/lib/projects/files/application/content.ts b/apps/sim/lib/projects/files/application/content.ts index 73b4005d11d..0bde55c3489 100644 --- a/apps/sim/lib/projects/files/application/content.ts +++ b/apps/sim/lib/projects/files/application/content.ts @@ -43,7 +43,7 @@ import { import { parseWorkspaceFileRevision } from '@/lib/workspace-files/application/file-revision' import { resolveWorkspaceFileVersionWrite } from '@/lib/workspace-files/application/file-version-write' import { MAX_WORKSPACE_FILE_CONTENT_BYTES } from '@/lib/workspace-files/orchestration' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { restoreSimPageSourceBuffer } from '@/lib/workspace-files/page-source-embed' interface ContentUseCaseArgs { diff --git a/apps/sim/lib/projects/files/application/downloads.ts b/apps/sim/lib/projects/files/application/downloads.ts index 9c9042e5085..543b8adf75a 100644 --- a/apps/sim/lib/projects/files/application/downloads.ts +++ b/apps/sim/lib/projects/files/application/downloads.ts @@ -59,7 +59,7 @@ import { } from '@/lib/workspace-files/download-selection' import { MAX_ZIP_DOWNLOAD_BYTES, MAX_ZIP_DOWNLOAD_FILES } from '@/lib/workspace-files/limits' import { fileOwnerCondition } from '@/lib/workspace-files/ownership-query' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { createFileReadReceipt, type FileReadReceipt, diff --git a/apps/sim/lib/projects/files/application/uploads.ts b/apps/sim/lib/projects/files/application/uploads.ts index 188904907a1..601278e8c7c 100644 --- a/apps/sim/lib/projects/files/application/uploads.ts +++ b/apps/sim/lib/projects/files/application/uploads.ts @@ -56,7 +56,7 @@ import { type UploadSessionRecord, } from '@/lib/uploads/upload-session/service' import type { WorkspaceFileUploadSource } from '@/lib/uploads/upload-session/workspace-file-provenance' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { MAX_SIM_PAGE_UPLOAD_SNIFF_BYTES, restoreSimPageSourceBuffer, diff --git a/apps/sim/lib/public-shares/application/read.ts b/apps/sim/lib/public-shares/application/read.ts index 641c227981e..9b881e76820 100644 --- a/apps/sim/lib/public-shares/application/read.ts +++ b/apps/sim/lib/public-shares/application/read.ts @@ -37,11 +37,8 @@ import { resolveEffectiveMimeType } from '@/lib/uploads/utils/file' import { sniffImageContentType } from '@/lib/uploads/utils/validation' import { reportWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer' import { fileOwnerCondition } from '@/lib/workspace-files/ownership-query' -import { - isSimPageSource, - ProjectPageForeignEmbedError, - SIM_PAGE_CONTENT_TYPE, -} from '@/lib/workspace-files/page-compile' +import { isSimPageSource, ProjectPageForeignEmbedError } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { renderSimPageDocument } from '@/lib/workspace-files/page-document' import { collectSimPageFileReferences, diff --git a/apps/sim/lib/uploads/client/download.test.ts b/apps/sim/lib/uploads/client/download.test.ts index 3d6311b5efa..c8865eae7a0 100644 --- a/apps/sim/lib/uploads/client/download.test.ts +++ b/apps/sim/lib/uploads/client/download.test.ts @@ -169,11 +169,18 @@ describe('large file downloads', () => { expect(createObjectURL).toHaveBeenCalledOnce() }) - it('keeps fetching a large generated document, which must compile before it can be served', async () => { - await triggerFileDownload({ ...video, name: 'report.pdf', type: 'text/x-pdflibjs' }) - - expect(fetchMock).toHaveBeenCalledOnce() - expect(fetchMock.mock.calls[0]?.[1]).not.toHaveProperty('headers') - expect(createObjectURL).toHaveBeenCalledOnce() - }) + it.each([ + ['a generated document', { name: 'report.pdf', type: 'text/x-pdflibjs' }], + ['a page recorded as one', { name: 'guide', type: 'text/x-sim-page' }], + ['an .html page', { name: 'guide.html', type: 'text/html' }], + ])( + 'keeps fetching %s, which the server renders before it can be served', + async (_label, kind) => { + await triggerFileDownload({ ...video, ...kind }) + + expect(fetchMock).toHaveBeenCalledOnce() + expect(fetchMock.mock.calls[0]?.[1]).not.toHaveProperty('headers') + expect(createObjectURL).toHaveBeenCalledOnce() + } + ) }) diff --git a/apps/sim/lib/uploads/client/download.ts b/apps/sim/lib/uploads/client/download.ts index fced8c56b5d..4f2b5bcd769 100644 --- a/apps/sim/lib/uploads/client/download.ts +++ b/apps/sim/lib/uploads/client/download.ts @@ -3,8 +3,9 @@ import { requestRaw } from '@/lib/api/client/request' import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' import { exportWorkspaceFileSnapshotContract } from '@/lib/api/contracts/workspace-files' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' -import { isMarkdownFile, needsRenderedArtifact } from '@/lib/uploads/utils/file' +import { isMarkdownFile } from '@/lib/uploads/utils/file' import { normalizeMimeType } from '@/lib/uploads/utils/mime' +import { servesStoredBytes } from '@/lib/workspace-files/stored-bytes' /** Action-time content from the mounted viewer, scoped so another file cannot consume it. */ export interface FileDownloadSource { @@ -16,9 +17,9 @@ export interface FileDownloadSource { /** * Above this a stored file is handed to the browser's download manager instead of being read into * the page. Holding it as a `Blob` costs its whole size in tab memory and shows no progress until - * the last byte lands; the download manager streams it to disk. Below it, and for a generated - * document, which must compile first, fetching keeps a rejection on screen as an error rather than - * as a failed entry in the browser's downloads list. + * the last byte lands; the download manager streams it to disk. Below it, and for a file the + * server renders first (a page, a generated document), fetching keeps a rejection on screen + * rather than as a failed entry in the browser's downloads list. */ const IN_PAGE_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024 @@ -99,7 +100,7 @@ export async function triggerFileDownload( if ( !exportsMarkdown && - !needsRenderedArtifact(record.type, record.name) && + servesStoredBytes(record.type, record.name) && record.size > IN_PAGE_DOWNLOAD_MAX_BYTES ) { // A first-byte read opens storage itself, so a missing object is refused here, in place, diff --git a/apps/sim/lib/uploads/contexts/workspace/file-manager-page.test.ts b/apps/sim/lib/uploads/contexts/workspace/file-manager-page.test.ts index cea39a3e947..f9ee575954e 100644 --- a/apps/sim/lib/uploads/contexts/workspace/file-manager-page.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/file-manager-page.test.ts @@ -20,7 +20,8 @@ vi.mock('@/lib/uploads/contexts/workspace/file-folder-manager', () => workspaceF import { createWorkspaceFileBodySchema } from '@/lib/api/contracts/workspace-files' import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/file-manager' -import { compileSimPage, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { compileSimPage } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' workspaceFileFoldersMockFns.mockBuildWorkspaceFileFolderPathMap.mockImplementation(() => new Map()) workspaceFileFoldersMockFns.mockNormalizeWorkspaceFileItemName.mockImplementation( diff --git a/apps/sim/lib/uploads/contexts/workspace/file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/file-manager.ts index c0b6c9078f4..79d18ab975b 100644 --- a/apps/sim/lib/uploads/contexts/workspace/file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/file-manager.ts @@ -161,7 +161,7 @@ import { resolveFileOwner, } from '@/lib/workspace-files/ownership' import { fileFolderOwnerCondition, fileOwnerCondition } from '@/lib/workspace-files/ownership-query' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { MAX_SIM_PAGE_UPLOAD_SNIFF_BYTES, restoreSimPageSourceBuffer, diff --git a/apps/sim/lib/uploads/utils/file.server.ts b/apps/sim/lib/uploads/utils/file.server.ts index 0f5cb1180e6..ce80eff8c88 100644 --- a/apps/sim/lib/uploads/utils/file.server.ts +++ b/apps/sim/lib/uploads/utils/file.server.ts @@ -35,7 +35,8 @@ import { type RawFileInput, resolveTrustedFileContext, } from '@/lib/uploads/utils/file' -import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { isSimPageSource } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { renderSimPageDocumentWithContributors } from '@/lib/workspace-files/page-document.server' import { type KnowledgeFileAccess, verifyFileAccess } from '@/app/api/files/authorization' import type { UserFile } from '@/executor/types' diff --git a/apps/sim/lib/workspace-files/application/stream-authorized-workspace-file.ts b/apps/sim/lib/workspace-files/application/stream-authorized-workspace-file.ts index 0ece44596e8..3caec548ea1 100644 --- a/apps/sim/lib/workspace-files/application/stream-authorized-workspace-file.ts +++ b/apps/sim/lib/workspace-files/application/stream-authorized-workspace-file.ts @@ -11,6 +11,10 @@ import { createFileReadReceipt } from '@/lib/workspace-files/read-receipt' * Opens an already-authorized workspace file's stored bytes for delivery, whole or as one inclusive * span, without reading them into memory. * + * Callers must already have authorized and admitted the file (`'opaque'`) through their own use + * case: storage is opened before this function's own recheck, which only decides whether the + * opened stream is handed out. + * * Access is rechecked once storage has opened and before the stream is handed out, as every * streamed read does: a revocation, a content write, or a provenance change landing in between * disposes the stream instead of publishing it. `authorize` is the caller's own use-case check, so diff --git a/apps/sim/lib/workspace-files/archive-extraction.ts b/apps/sim/lib/workspace-files/archive-extraction.ts index 2530f27821d..e057d36501a 100644 --- a/apps/sim/lib/workspace-files/archive-extraction.ts +++ b/apps/sim/lib/workspace-files/archive-extraction.ts @@ -11,7 +11,7 @@ import { import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/file-secret-provenance' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file' import type { EditableFileOwner } from '@/lib/workspace-files/ownership' -import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' import { restoreSimPageSourceBuffer } from '@/lib/workspace-files/page-source-embed' interface StagedArchiveEntry { diff --git a/apps/sim/lib/workspace-files/page-compile.ts b/apps/sim/lib/workspace-files/page-compile.ts index 6c41cd3da03..5887ad6d03e 100644 --- a/apps/sim/lib/workspace-files/page-compile.ts +++ b/apps/sim/lib/workspace-files/page-compile.ts @@ -36,15 +36,6 @@ import type { EditableFileOwner } from '@/lib/workspace-files/ownership' */ export const SIM_PAGE_MARKER = '' -/** - * The INTERNAL content type stamped onto a page file's record when the write - * path detects page source. The file stays `.html` to the user (serving and - * downloads emit text/html); the record type is how surfaces know the file's - * nature before any content loads — force the rendered view, hide the code - * toggle, expect source. - */ -export const SIM_PAGE_CONTENT_TYPE = 'text/x-sim-page' - const frontmatterSchema = z.object({ title: z.string().min(1), /** Tolerated for old sources; no longer rendered — the docs have no eyebrow. */ diff --git a/apps/sim/lib/workspace-files/page-content-type.ts b/apps/sim/lib/workspace-files/page-content-type.ts new file mode 100644 index 00000000000..69c4b5411d4 --- /dev/null +++ b/apps/sim/lib/workspace-files/page-content-type.ts @@ -0,0 +1,11 @@ +/** + * The INTERNAL content type stamped onto a page file's record when the write + * path detects page source. The file stays `.html` to the user (serving and + * downloads emit text/html); the record type is how surfaces know the file's + * nature before any content loads — force the rendered view, hide the code + * toggle, expect source. + * + * Kept free of the page compiler so client surfaces that only need to recognize a page + * do not load the compiler's parsers. + */ +export const SIM_PAGE_CONTENT_TYPE = 'text/x-sim-page' diff --git a/apps/sim/lib/workspace-files/stored-bytes.ts b/apps/sim/lib/workspace-files/stored-bytes.ts new file mode 100644 index 00000000000..d3a16fdbe30 --- /dev/null +++ b/apps/sim/lib/workspace-files/stored-bytes.ts @@ -0,0 +1,21 @@ +import { needsRenderedArtifact } from '@/lib/uploads/utils/file' +import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type' + +/** + * Whether serving a file hands back its stored object byte-for-byte, rather than a representation + * produced from it: a Sim page renders and a generated document compiles. The file-serve route + * streams exactly these, and the client hands exactly these to the browser's download manager, so + * one answer keeps the two from disagreeing about which files a probe may vouch for. + * + * A recorded type tells a finished upload from a generation source; without one, every document + * name may be a source. + */ +export function servesStoredBytes( + contentType: string | null | undefined, + fileName: string +): boolean { + if (contentType === SIM_PAGE_CONTENT_TYPE || fileName.toLowerCase().endsWith('.html')) { + return false + } + return !needsRenderedArtifact(contentType, fileName) +}