@@ -28,20 +28,22 @@ import {
2828 workspaceFileCacheControl ,
2929} from '@/lib/uploads/server/delivery'
3030import { resolveServableImageBytes } from '@/lib/uploads/server/image-derivative'
31- import { resolveStoredFileContext } from '@/lib/uploads/server/metadata'
31+ import { getFileMetadataByKey , resolveStoredFileContext } from '@/lib/uploads/server/metadata'
3232import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
3333import {
3434 inferContextFromKey ,
35- needsRenderedArtifact ,
35+ isRenderableDocumentName ,
3636 resolveEffectiveMimeType ,
3737} from '@/lib/uploads/utils/file'
3838import { internalWorkspaceFileServeAuth } from '@/lib/workspace-files/api'
3939import { fetchAuthorizedServableWorkspaceFileBuffer } from '@/lib/workspace-files/application/fetch-servable-workspace-file-buffer'
4040import { finishFileDelivery } from '@/lib/workspace-files/application/finish-file-delivery'
4141import { readWorkspaceFileRecordByKey } from '@/lib/workspace-files/application/read-workspace-file-record-by-key'
4242import { streamAuthorizedWorkspaceFile } from '@/lib/workspace-files/application/stream-authorized-workspace-file'
43- import { isSimPageSource , SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile'
43+ import { isSimPageSource } from '@/lib/workspace-files/page-compile'
44+ import { SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-content-type'
4445import { renderSimPageDocumentWithAssets } from '@/lib/workspace-files/page-document.server'
46+ import { servesStoredBytes } from '@/lib/workspace-files/stored-bytes'
4547import { type KnowledgeFileAccess , verifyFileAccess } from '@/app/api/files/authorization'
4648import {
4749 createConditionalFileResponse ,
@@ -203,10 +205,10 @@ async function resolveTransformedBytes(params: {
203205 * Whether a request is answered with the stored object byte-for-byte, so its bytes can stream from
204206 * storage instead of being read whole and capped at the transfer ceiling.
205207 *
206- * Every other answer is produced from the bytes and stays buffered: a Sim page renders, a generated
207- * document compiles, and a `preview` may be a HEIC transcode — sniffed from the content, so no name
208- * or type rules it out. What streams is exactly what {@link resolveServableBytes} and the
209- * workspace resolver would have passed through unchanged.
208+ * Every other answer is produced from the bytes and stays buffered: see { @link servesStoredBytes},
209+ * plus a `preview`, which may be a HEIC transcode — sniffed from the content, so no name or type
210+ * rules it out. What streams is exactly what {@link resolveServableBytes} and the workspace
211+ * resolver would have passed through unchanged.
210212 */
211213function servesStoredObject (
212214 filename : string ,
@@ -215,12 +217,7 @@ function servesStoredObject(
215217) : boolean {
216218 if ( options . raw ) return true
217219 if ( options . preview ) return false
218- if ( recordedType === SIM_PAGE_CONTENT_TYPE || filename . toLowerCase ( ) . endsWith ( '.html' ) ) {
219- return false
220- }
221- // A recorded type tells a finished upload from a generation source; without one, every
222- // document name may be a source.
223- return ! needsRenderedArtifact ( recordedType , filename )
220+ return servesStoredBytes ( recordedType , filename )
224221}
225222
226223/**
@@ -231,11 +228,26 @@ function storedObjectContentType(filename: string, options: ServeOptions): strin
231228 return options . raw ? getContentType ( filename ) : resolveEffectiveMimeType ( undefined , filename )
232229}
233230
231+ /**
232+ * The type recorded for an object outside the workspace-file model, read only where it decides the
233+ * answer: a document name may be a finished upload or a generation source, and the object's file
234+ * record, where it has one, says which. An object with no record is judged by its name.
235+ */
236+ async function recordedDocumentType (
237+ key : string ,
238+ context : StorageContext ,
239+ filename : string ,
240+ options : ServeOptions
241+ ) : Promise < string | undefined > {
242+ if ( options . raw || options . preview || ! isRenderableDocumentName ( filename ) ) return undefined
243+ return ( await getFileMetadataByKey ( key , context ) ) ?. contentType
244+ }
245+
234246/**
235247 * Streams a stored object outside the workspace-file model (execution outputs, chat and copilot
236248 * attachments, knowledge documents) when the request is answered with it as-is, or returns null
237- * for the buffered path. The object is its only record, so storage is asked for its length and
238- * version, and the version is what proves a revalidation still holds.
249+ * for the buffered path. Storage is asked for the object's length and version, and the version is
250+ * what proves a revalidation still holds.
239251 */
240252async function streamStoredObject ( params : {
241253 key : string
@@ -245,7 +257,8 @@ async function streamStoredObject(params: {
245257 request : NextRequest
246258} ) : Promise < NextResponse | null > {
247259 const { key, context, filename, options } = params
248- if ( ! servesStoredObject ( filename , undefined , options ) ) return null
260+ const recordedType = await recordedDocumentType ( key , context , filename , options )
261+ if ( ! servesStoredObject ( filename , recordedType , options ) ) return null
249262 const stored = await headObject ( key , context )
250263 if ( ! stored ) throw new FileNotFoundError ( `File not found: ${ key } ` )
251264
0 commit comments