Files & Object Storage
Object storage on the same two scopes as tables: the private scope (the current
user) and the resource scope (a shared boundary). Files are path-addressed;
blobs live in the storage adapter (a local directory in dev, S3-compatible
later) keyed opaquely, while the _files metadata table is the source of truth
for access, listing, and metering.
osgarden.files("avatars") // private scope
osgarden.resource(roomId).files("shared") // resource scopeBuckets are declared in the schema (buckets at the top level, resource.files(...)
inside a resource type). See schema-and-deploy.md.
Methods#
.upload(path, file, options?) // options: { contentType, cacheControl, upsert }
.download(path, { owner? }?) // → Blob
.url(path, { owner? }?) // → string, same-origin cookie-authed (no request)
.list(prefix?) // → FileMetadata[]
.metadata(path, { owner? }?) // → FileMetadata
.updateMetadata(path, { contentType?, cacheControl? })
.delete(path, { owner? }?)
.move(fromPath, toPath) // same scope only (metadata-only; blobs are id-addressed)
.copy(fromPath, toPath).url() returns a same-origin, cookie-authenticated URL for use directly as an
<img src> — cookie-authed GET/HEAD on a file blob is exempt from the CSRF
header (safe under SameSite=Lax, where a cross-site request never carries the
cookie). It performs no request; it just builds the string.
Access#
- Private buckets are owner-only: every op is scoped to the caller's own files. Top-level buckets carry no access rules.
- Resource buckets evaluate their declared
FileAccessIR(list/read/upload/update/delete) with the same default-deny engine as tables. Thecreatorvalue means "the uploader" and scopes a read/update/delete/list to the caller's own files; a role orpublicgrants a blanket allow.
Within a shared bucket, paths are namespaced by the uploader: two members
uploading beach.jpg do not collide. list() returns all owners' files when the
rule is a blanket allow (each entry carries owner); reading or deleting another
member's file passes { owner } and requires that blanket allow.
Metering#
- Upload places a one-time
storage_writeescrow hold on the at-rest payer (a resource bucket withpayer: "owner"bills the resource owner; otherwise the uploader), enforces the bucket'sacceptandmaxFileSizemid-stream (413 on overflow), and captures the actual byte cost. - Download meters
storage_egressto the fetching session.
The recurring GB-month at-rest sweep is deferred; the upload charge is padded to amortize retention. See metering.md.
Cascades#
Deleting a resource, or removing/leaving a member, deletes the relevant files and
their blobs. File buckets carry no per-bucket lifecycle yet, so member removal
uses the remove default (the hidden column and read-filtering are in place
for future hide/retain).
Errors#
not_found (unknown bucket/file, or a members-only resource to a non-member),
forbidden (access denied, or targeting another member's file without a blanket
allow), conflict (upload to an existing path without upsert; move/copy onto
an existing path), invalid_request (rejected content-type, over the size
limit), insufficient_balance / spend_cap_exceeded (402, on upload).