Skip to main content

createFileService

@forge/monorepo


@forge/monorepo / backend/src / createFileService

Function: createFileService()

createFileService(deps): object

Defined in: backend/src/files/index.ts:176

The attachment lifecycle.

Every method takes an ExecutionContext, and entitlement to a file is entitlement to its conversation — so there is no second permission model to keep in step with the first.

Parameters

deps

FileServiceDeps

Returns

limits

limits: UploadLimits

deleteConversationFiles()

deleteConversationFiles(context, conversationId): Promise<{ scheduled: number; }>

AC-4: deleting a conversation removes the metadata and schedules the bytes.

Two steps, and they cannot be one: object storage does not join a database transaction. So the metadata moves to deleting — gone from the user's view — and sweepDeletions removes the bytes afterwards. The intermediate state is named rather than pretended away.

Parameters

context

ExecutionContext

conversationId

ConversationId

Returns

Promise<{ scheduled: number; }>

get()

get(context, id): Promise<FileMetadata>

AC-3: only through the owning conversation, and a foreign file is not_found, never forbidden.

Parameters

context

ExecutionContext

id

FileId

Returns

Promise<FileMetadata>

listForConversation()

listForConversation(context, input): Promise<Page<FileMetadata>>

The listing is authorised on the conversation before it runs, not filtered afterwards.

Filtering results would mean the query ran, which for an unentitled caller is a query whose shape — a page count, a cursor, a timing difference — still answers "does this conversation have files".

Parameters

context

ExecutionContext

input
conversationId

ConversationId

cursor?

string

limit

number

Returns

Promise<Page<FileMetadata>>

read()

read(context, id): Promise<AsyncIterable<Uint8Array<ArrayBufferLike>, any, any>>

The proxied read, for an adapter that cannot sign — and the fallback when one can.

Parameters

context

ExecutionContext

id

FileId

Returns

Promise<AsyncIterable<Uint8Array<ArrayBufferLike>, any, any>>

signedReadUrl()

signedReadUrl(context, id): Promise<string | null>

A short-lived URL, or a stream when the adapter proxies.

The expiry is clamped rather than trusted: a caller asking for a day gets fifteen minutes. AC-6 is about what is reachable, and a caller's optimism is not a reason to widen it.

Parameters

context

ExecutionContext

id

FileId

Returns

Promise<string | null>

sweepDeletions()

sweepDeletions(context, input): Promise<{ deleted: number; failed: number; }>

Remove the bytes of files marked deleting.

Bytes first, then the state — the opposite order from upload, and for the mirrored reason. Marking deleted first means a crash leaves an object nothing references and nothing will look for again. Deleting bytes first means a crash leaves the row in deleting, and the next sweep retries it; deleteFile is idempotent so the retry costs nothing.

Parameters

context

ExecutionContext

input
limit

number

olderThan

string

Returns

Promise<{ deleted: number; failed: number; }>

upload()

upload(context, input): Promise<FileMetadata>

Upload: validate, record pending, write bytes, then move to stored.

The order is the point, and the alternative is worse in a specific way. Writing bytes first and metadata second means a crash between them leaves an object nothing references — invisible, and billed for. Metadata first means a crash leaves a row in pending, which reconciliation can see and a user cannot. An orphan you can find beats an orphan you cannot.

Parameters

context

ExecutionContext

input
bytes

AsyncIterable<Uint8Array<ArrayBufferLike>>

conversationId

ConversationId

declaredBytes

number

filename

string

mediaType

string

Returns

Promise<FileMetadata>