Search pages, headings and code across the docs, examples and tutorials.

to navigateto open
Uploads

Storage

The filesystem, S3, or a backend of your own.

A file is stored under its path, photos/2024/sunset.jpg, and a folder is a prefix. The fs backend keeps that layout under uploads.url, .uploads by default, resolved against your app's root. The UPLOADS_URL env var overrides url.

Storing files in S3

For S3 and S3-compatible services, install @ohnejs/s3.

Terminal
pnpm add @ohnejs/s3
ohne.config.ts
import { defineConfig } from 'ohnejs';

export default defineConfig({
  layers: ['ohnejs/base', 'ohnejs/uploads', '@ohnejs/s3'],
  uploads: { storage: 's3', url: 's3://my-bucket/uploads?region=eu-central-1' },
});

Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. The package README covers bucket setup, private files, and services such as R2 and MinIO.

A backend of your own

Register a backend from a boot file and select it by name:

boot/storage.ts
import { useStorages } from 'ohnejs/uploads';

import { createGCSStorage } from '../storage/gcs.ts';

useStorages().register('gcs', (url) => createGCSStorage(url));
ohne.config.ts
import { defineConfig } from 'ohnejs';

export default defineConfig({
  layers: ['ohnejs/base', 'ohnejs/uploads'],
  uploads: { storage: 'gcs', url: 'gs://my-bucket/uploads' },
});

A backend implements StorageAdapter; its JSDoc names each method. The rules it cannot show:

  • move and delete take a prefix as well as a file, so a folder is one call.
  • The layer journals each effect inside the row's transaction and replays it after a crash, so a missing path must be a no-op.
  • setPrivate locks an object a backend serves itself, as a row turns private. Without it, a private object stays readable at publicURL, and ohne warns at boot.
  • A backend that serves its files itself stores the disposition that write receives, as @ohnejs/s3 does. Without it, an uploaded HTML file runs as a page on that origin.
  • list lets ohne uploads prune find stray files. Without it, the command refuses.
  • parts lets a file go up in resumable chunks. Without it, opening a session answers 501, and the dashboard sends every file whole.

Stray files

A file can end up in storage with no row behind it, after a database restored from an older backup or a file copied in by hand. The ohne uploads prune command lists those files:

Terminal
npx ohne uploads prune

Once the list holds only files you can lose, --delete deletes them. A file that is still uploading is never among them. Nor is an unfinished resumable upload: once its session expires, the next boot or this command sweeps its chunks away.

An upload cut off by a crash, a SIGKILL, or a passed deadline leaves a partial file under .tmp/, and the first boot once a day has passed deletes it.

storage and url are each layer's own: a dependency cannot point your uploads at its storage.