files
Per-app blob storage with S3-shaped semantics: identity by slug, PUT replaces, metadata beside the bytes. For when the file-store is the right home, see the File Store.
import { files } from "@lodekit/sdk";Works in the browser and in server functions. Bytes stream in both directions and have no size cap — the scoped API’s 100 kb request cap does not apply to file bodies. TTLs are in seconds; timestamps are milliseconds since the epoch.
files.put()
Section titled “files.put()”put( body: FilePutBody, opts?: { slug?: string; title?: string; contentType?: string; ttl?: number; metadata?: Record<string, unknown> }): Promise<FileStat>body— the bytes:string | Blob | ArrayBuffer | Uint8Array | ReadableStream. AReadableStreamuploads without buffering.opts.slug— explicit identity. When present, this is a create-or-replace write (S3 PUT semantics): an existing file at that slug is replaced. When absent, the server derives a slug from the title or filename, suffixing-2,-3, … on collision.opts.title— display title, ≤ 512 characters.opts.contentType— MIME type. Defaultapplication/octet-stream.opts.ttl— time to live in seconds.opts.metadata— a JSON object stored beside the bytes, ≤ 4 KB.
Returns the stored file’s FileStat.
Example
Section titled “Example”Greenhouse’s photo upload (browser, from a file input):
await files.put(file, { slug: `photo-${id}-${invTs(Date.now())}`, title: caption || file.name, contentType: file.type || "application/octet-stream", metadata: { plantId: id },});And a self-expiring export (server function):
const stat = await files.put( JSON.stringify({ exportedAt: Date.now(), stats, plants: allPlants }, null, 2), { slug: `snapshot-${Date.now()}`, contentType: "application/json", title: "Conservatory snapshot", ttl: 3600 });files.get()
Section titled “files.get()”get(slug: string): Promise<Blob | undefined>slug— the file’s slug.
Returns the bytes as a Blob, or undefined if the file doesn’t exist or its
TTL has expired.
Example
Section titled “Example”const blob = await files.get("cover-3");if (blob) { const text = await blob.text();}files.url()
Section titled “files.url()”url(slug: string, opts?: { download?: boolean }): stringslug— the file’s slug.opts.download— appends?download=1so browsers save instead of render.
Synchronous — builds the file’s public GET URL without a network call, so you can use it directly in markup. The URL responds 404 if no such file exists.
Example
Section titled “Example”<img src={files.url(photo.slug)} alt={photo.title ?? ""} />files.stat()
Section titled “files.stat()”stat(slug: string): Promise<FileStat | undefined>slug— the file’s slug.
Returns the file’s metadata without fetching the bytes, or undefined if it
doesn’t exist or has expired.
Example
Section titled “Example”const stat = await files.stat(`cover-${id}`);if (stat) logs.info("cover size", { bytes: stat.size });files.delete()
Section titled “files.delete()”delete(slug: string): Promise<boolean>slug— the file’s slug.
Deletes the file — row and bytes. Returns true if a file was deleted, false
if it didn’t exist.
Example
Section titled “Example”await files.delete(`snapshot-${staleId}`);files.list()
Section titled “files.list()”list(opts?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ entries: FileStat[]; nextCursor: string | null }>opts.prefix— only slugs starting with this prefix.opts.limit— page size. Default 100 (thefiles.list_limitplatform setting), clamped to 1–1000.opts.cursor— resume after this slug.
Returns entries ordered by slug ascending. Pagination is keyset: nextCursor is
the last slug of the page — pass it back as cursor; null means done.
Example
Section titled “Example”const { entries } = await files.list({ prefix: `photo-${id}-` });files.copy()
Section titled “files.copy()”copy(from: string, to: string): Promise<FileStat>from— the source slug.to— the destination slug.
Duplicates a file, replacing any existing file at to. The copy is a new file:
createdAt and updatedAt are reset. Returns the destination’s FileStat.
Example
Section titled “Example”Greenhouse sets a plant’s cover photo by copying:
await files.copy(f.slug, `cover-${id}`);files.move()
Section titled “files.move()”move(from: string, to: string): Promise<FileStat>from— the current slug.to— the new slug.
Renames the file in place — same file, new identity — preserving createdAt.
Returns the file’s FileStat under its new slug.
Example
Section titled “Example”Greenhouse archives a photo by renaming it out of the gallery prefix:
await files.move(f.slug, f.slug.replace(`photo-${id}-`, `archive-${id}-`));A slug is the file’s identity: 1–128 characters matching ^[a-z0-9-]{1,128}$.
When put is called without a slug, the server derives one from the title or
filename: NFKD-normalize, strip diacritics, lowercase, turn runs of
non-alphanumerics into -, trim, cap at 128 characters; an empty result becomes
"file". Derived slugs get -2, -3, … appended on collision. An explicit
slug that already exists is a replace, never a suffix.
Behavior notes
Section titled “Behavior notes”- TTL expiry is lazy — an expired file is removed (row and bytes) when it’s next read, and reported missing.
- Bytes are real files on disk — under
data/<app-id>/files/<YYYY-MM-DD>/<slug>-<hash8>.<ext>, browsable in Finder; SQLite holds the authoritative index.statandgetreconcile files deleted outside Lodekit. - Every change emits an event —
file-store-changewith a payload of{ slug, op }whereopis"put","del", or"expire". See events.
FileStat
Section titled “FileStat”interface FileStat { slug: string; title: string | null; contentType: string; size: number; hash: string; metadata: Record<string, unknown> | null; createdAt: number; updatedAt: number; expiresAt: number | null; url: string; path: string;}slug— the file’s identity.title— display title, ornull.contentType— the stored MIME type.size— byte count.hash— sha256 of the bytes, hex-encoded.metadata— the stored JSON object, ornull.createdAt/updatedAt— ms-epoch timestamps.expiresAt— ms-epoch expiry, ornullfor files without a TTL.url— the public GET URL (whatfiles.url()returns).path— the absolute on-disk path of the bytes.
FilePutBody
Section titled “FilePutBody”type FilePutBody = string | Blob | ArrayBuffer | Uint8Array | ReadableStream;Anything put accepts as bytes.