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 scope

Buckets 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#

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#

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).