d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
import { createContext, useContext } from 'react'
|
|
import {
|
|
type EmbeddedFileRef,
|
|
extractEmbeddedFileRef,
|
|
} from '@/lib/uploads/utils/embedded-image-ref'
|
|
|
|
export interface FileContentUrlOptions {
|
|
/** Request the uncompiled source instead of the rendered/compiled bytes. */
|
|
raw?: boolean
|
|
/** Content version (e.g. the record's `updatedAt`) — makes the URL cacheable/immutable. */
|
|
version?: string | number
|
|
/** Append a timestamp cache-buster when there is no `version`. */
|
|
bust?: boolean
|
|
}
|
|
|
|
function inlineRefQuery(ref: NonNullable<EmbeddedFileRef>): string {
|
|
return 'key' in ref
|
|
? `key=${encodeURIComponent(ref.key)}`
|
|
: `fileId=${encodeURIComponent(ref.fileId)}`
|
|
}
|
|
|
|
/**
|
|
* Seam for "where do a file's bytes come from". The in-app viewer resolves the
|
|
* auth-gated workspace serve URL; the public share page swaps in a token-scoped
|
|
* URL. Renderers and the binary/text query hooks build their fetch URL through
|
|
* this source so the same components work in both contexts.
|
|
*/
|
|
export interface FileContentSource {
|
|
buildUrl: (key: string, opts?: FileContentUrlOptions) => string
|
|
/**
|
|
* Map an embedded image `src` to a display URL scoped to the current context: the in-app source
|
|
* points at the workspace-scoped inline route, the public source at the token-scoped cascade route.
|
|
* Non-workspace srcs (external, `data:`, public assets) pass through unchanged.
|
|
*/
|
|
resolveImageSrc: (src: string | undefined) => string | undefined
|
|
}
|
|
|
|
function buildServeUrl(key: string, opts?: FileContentUrlOptions): string {
|
|
const base = `/api/files/serve/${encodeURIComponent(key)}?context=workspace`
|
|
const params: string[] = []
|
|
if (opts?.version != null) params.push(`v=${encodeURIComponent(String(opts.version))}`)
|
|
else if (opts?.bust) params.push(`t=${Date.now()}`)
|
|
if (opts?.raw) params.push('raw=1')
|
|
return params.length > 0 ? `${base}&${params.join('&')}` : base
|
|
}
|
|
|
|
/** Build a source whose embeds resolve through `inlineBase` (the workspace- or token-scoped inline route). */
|
|
function inlineImageSource(
|
|
buildUrl: FileContentSource['buildUrl'],
|
|
inlineBase: string
|
|
): FileContentSource {
|
|
return {
|
|
buildUrl,
|
|
resolveImageSrc: (src) => {
|
|
if (!src) return src
|
|
const ref = extractEmbeddedFileRef(src)
|
|
return ref ? `${inlineBase}?${inlineRefQuery(ref)}` : src
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* In-app source scoped to one workspace. Direct file bytes come from the workspace serve URL; embedded
|
|
* images route through `/api/workspaces/{workspaceId}/files/inline`, which resolves a reference only
|
|
* within this workspace — a cross-workspace embed 404s and does not render.
|
|
*/
|
|
export function createWorkspaceFileContentSource(workspaceId: string): FileContentSource {
|
|
return inlineImageSource(buildServeUrl, `/api/workspaces/${workspaceId}/files/inline`)
|
|
}
|
|
|
|
/**
|
|
* Public share source. Direct file bytes come from the token content URL; embedded images route through
|
|
* `/api/files/public/{token}/inline`, which serves them only when referenced by the shared document and
|
|
* in its workspace.
|
|
*/
|
|
export function createPublicFileContentSource(
|
|
token: string,
|
|
contentUrl: string
|
|
): FileContentSource {
|
|
return inlineImageSource(() => contentUrl, `/api/files/public/${token}/inline`)
|
|
}
|
|
|
|
/**
|
|
* Context default for components rendered outside a {@link FileContentSourceProvider}: serve URLs for
|
|
* direct bytes, embeds passed through unchanged. The file viewer always provides a workspace- or
|
|
* token-scoped source, so embeds resolve through the scoped inline routes there.
|
|
*/
|
|
export const workspaceFileContentSource: FileContentSource = {
|
|
buildUrl: buildServeUrl,
|
|
resolveImageSrc: (src) => src,
|
|
}
|
|
|
|
const FileContentSourceContext = createContext<FileContentSource>(workspaceFileContentSource)
|
|
|
|
export const FileContentSourceProvider = FileContentSourceContext.Provider
|
|
|
|
export function useFileContentSource(): FileContentSource {
|
|
return useContext(FileContentSourceContext)
|
|
}
|