createFileService
@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
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
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
id
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
input
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
id
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
id
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
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
input
bytes
AsyncIterable<Uint8Array<ArrayBufferLike>>
conversationId
declaredBytes
number
filename
string
mediaType
string
Returns
Promise<FileMetadata>