RustFS
RustFS, the Apache-2.0 Rust object store that runs as a MinIO drop-in. Path-style addressing on by default; region defaulted; errors relabelled; optional @aws-sdk-free fetch client for Workers.
Installation
The @aws-sdk/* packages are only needed for the "aws-sdk" client (the default outside Cloudflare Workers). The lightweight fetch client needs none of them - files-sdk alone is enough:
npm install files-sdkpnpm add files-sdkyarn add files-sdkbun add files-sdknub add files-sdkaube add files-sdkFor the "aws-sdk" client, @aws-sdk/client-s3, @aws-sdk/s3-presigned-post, and @aws-sdk/s3-request-presigner are optional peer dependencies - install alongside the SDK so the adapter’s imports resolve at runtime.
npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presignerpnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigneryarn add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presignerbun add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presignernub add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigneraube add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presignerWith the "aws-sdk" client, upload also needs the optional @aws-sdk/lib-storage package when it uses onProgress, multipart, or a ReadableStream body of unknown length. It’s only loaded then.
Usage
import { Files } from "files-sdk";
import { rustfs } from "files-sdk/rustfs";
const files = new Files({
adapter: rustfs({
bucket: "uploads",
endpoint: "http://localhost:9000",
// accessKeyId / secretAccessKey auto-loaded from
// RUSTFS_ACCESS_KEY_ID / RUSTFS_SECRET_ACCESS_KEY, falling back to the
// RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY names the server itself reads
}),
});
endpoint is the S3 API port (9000 by default); the RustFS console on 9001 is not an S3 endpoint. The env-var fallback accepts the same RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY names the server reads, so one docker-compose .env configures both sides. Rotate the well-known rustfsadmin defaults before exposing the server beyond localhost.
The "aws-sdk" client is loaded on first use, so files.raw is undefined until any method has run - call one first if you need the underlying S3Client.
Server defaults the adapter mirrors
- Path-style addressing is on. RustFS only accepts virtual-hosted requests (
<bucket>.<host>) onceRUSTFS_SERVER_DOMAINSand a wildcard DNS record are configured; passforcePathStyle: falseafter that. - Signing region defaults to
us-east-1, matching the server’sRUSTFS_REGIONdefault. If you changed that variable, pass the same value asregion- RustFS rejects signatures for another region. - Migrating from MinIO? The two servers share these defaults, so swapping
minio()forrustfs()is a rename plus the env-var prefix.
Lightweight fetch client
Pass client: "fetch" to swap the @aws-sdk/* stack for a SigV4-signed fetch engine built on aws4fetch (~2.5 KB gzipped, Web Crypto only). No @aws-sdk/* packages are installed or bundled - ideal for Cloudflare Workers and other edge runtimes where the AWS SDK’s ~500 KB defeats the point of web-standard tooling.
const files = new Files({
adapter: rustfs({
bucket: "uploads",
endpoint: "https://rustfs.internal:9000",
client: "fetch",
}),
});
Prefer this over pointing the generic s3Fetch() at a RustFS server: it is the same engine, but RustFS’s defaults ride along - path-style addressing (without RUSTFS_SERVER_DOMAINS, virtual-hosted requests surface as a misleading NoSuchBucket), the us-east-1 signing region, and RustFS error labels.
The fetch client covers upload, download (including ranges), head, exists, delete, list (including delimiter folding), server-side copy, presigned url(), and signedUploadUrl(). Trade-offs against the "aws-sdk" client:
ReadableStreambodies are buffered in memory before a single PUT (a lone PUT needs aContent-Length, and single-request uploads cap at 5 GB).multipartand resumable (control) uploads throw instead of engaging the S3 multipart API.- Bulk deletes fan out as per-key
delete()calls instead of batchedDeleteObjectsrequests. On Workers each call is a subrequest, so chunk large bulk deletes to stay under the per-invocation subrequest cap. signedUploadUrl()returns a presigned PUT;maxSizethrows because enforcing it needs a presigned POST policy this engine doesn’t implement.- Keys containing
.or..path segments are rejected - URL normalization would silently sign a request for a different key. - Byte-level
onProgressreporting falls back to the SDK’s generic reporting. files.rawis the aws4fetchAwsClient, not anS3Client.
Default inside Cloudflare Workers
Inside Cloudflare Workers the fetch client is the default. The "aws-sdk" client’s XML parsing needs a DOMParser, which workerd doesn’t provide, so it fails at runtime on the first list or error-body parse - long after construction. Detection uses navigator.userAgent === "Cloudflare-Workers", or the workerd-only WebSocketPair global on compatibility dates where navigator is disabled. Two things keep the "aws-sdk" default even in a Worker: an explicit client: "aws-sdk", and a DOMParser on the global (the usual polyfill workaround), since the SDK works once that is present.
Options
bucketstring
RustFS bucket name. The adapter scopes all operations to it.
stringendpointstring
RustFS server URL, e.g. `http://localhost:9000` (the S3 API port; the console lives on 9001). Include the scheme — `http://` for local dev, `https://` in production.
stringaccessKeyId?string
Static credentials. Falls back to `RUSTFS_ACCESS_KEY_ID`, then to the `RUSTFS_ACCESS_KEY` variable the RustFS server itself reads (so a docker-compose `.env` serves both sides); required if neither is set.
stringsecretAccessKey?string
Static credentials. Falls back to `RUSTFS_SECRET_ACCESS_KEY`, then to the `RUSTFS_SECRET_KEY` variable the RustFS server itself reads; required if neither is set.
stringregion?string
SigV4 region used for signing. Defaults to `us-east-1`, which is also the server's default (`RUSTFS_REGION`). Set it to match the server if you changed that variable — RustFS rejects signatures for a different region.
stringforcePathStyle?boolean
Use path-style addressing (`/<bucket>/<key>`) rather than virtual-hosted style. Defaults to `true` — RustFS serves path style out of the box and only accepts virtual-hosted requests once `RUSTFS_SERVER_DOMAINS` and a wildcard DNS record are configured.
booleanpublicBaseUrl?string
Origin used to build URLs from `url()`. When set, `url(key)` returns `${publicBaseUrl}/${key}` — appropriate for a public bucket policy or a reverse proxy in front of RustFS. When unset, `url()` falls back to a presigned GetObject (default expiry: 1 hour).
stringdefaultUrlExpiresIn?number
Default expiry, in seconds, for the presigned URLs returned by `url()` when `publicBaseUrl` is not set. Defaults to 3600 (1 hour).
numberclient?"aws-sdk" | "fetch"
Which HTTP engine backs the adapter. Defaults to `"aws-sdk"`, except on Cloudflare Workers (detected via `navigator.userAgent === "Cloudflare-Workers"`, or the workerd-only `WebSocketPair` global when `navigator` is disabled), where it defaults to `"fetch"` — the aws-sdk engine's XML parsing needs `DOMParser`, which workerd doesn't provide. A Worker that polyfills `DOMParser` keeps the `"aws-sdk"` default. Note the `"fetch"` engine's narrower surface below before relying on the default in a Worker. - `"aws-sdk"`: `@aws-sdk/client-s3` — the full surface, including multipart/resumable uploads, byte-level upload progress, and batched `deleteMany`. Requires the `@aws-sdk/*` optional peer dependencies. Loaded lazily on first use, so `raw` is `undefined` until a method has run. - `"fetch"`: SigV4-signed `fetch` via `aws4fetch` (~2.5 KB) — no `@aws-sdk/*` install needed, ideal for Workers and other edge runtimes. Covers upload, download (+ ranges), head, exists, delete, list (+ delimiter), server-side copy, presigned `url()`, and `signedUploadUrl()`. Trade-offs: `ReadableStream` bodies are buffered before the single PUT, `multipart`/`control` uploads throw, bulk deletes fan out per-key instead of batching, `signedUploadUrl` rejects `maxSize`, keys with `.`/`..` segments are rejected, and `raw` is an aws4fetch `AwsClient`. Both engines keep RustFS's defaults — path-style addressing, the `us-east-1` signing region, `RustFS error` labels — which is what reaching for the generic `s3Fetch()` instead would drop.
"aws-sdk" | "fetch"fetch?(request: Request) => Promise<Response>
Override the `fetch` implementation used by the `"fetch"` client — for tests, or runtimes that hand out a bound/instrumented fetch. Defaults to `globalThis.fetch`. Ignored by the `"aws-sdk"` client.
(request: Request) => Promise<Response>Compatibility
client: "aws-sdk" (default)
| Method | Status | Notes |
|---|---|---|
upload |
✅ | |
download |
✅ | |
delete |
✅ | |
list |
⚠️ | The S3 list API returns no per-object Content-Type, so type is inferred from the key’s extension (application/octet-stream when unknown). Use head() for the stored value. |
search |
✅ | |
head |
✅ | |
exists |
✅ | |
copy |
✅ | |
url |
✅ | |
signedUploadUrl |
⚠️ | maxSize switches to a presigned POST policy. RustFS’s own S3 compatibility notes list POST-object checksum handling as incomplete, so verify maxSize uploads against your server version before relying on them. |
client: "fetch"
| Method | Status | Notes |
|---|---|---|
upload |
⚠️ | Single PUT - ReadableStream bodies are buffered in memory first, and multipart / resumable control uploads throw. Multipart needs the "aws-sdk" client, which runs on Workers only with a DOMParser polyfill. |
download |
✅ | |
delete |
✅ | Bulk deletes fan out per key (no batched DeleteObjects). |
list |
⚠️ | The S3 list API returns no per-object Content-Type, so type is inferred from the key’s extension (application/octet-stream when unknown). Use head() for the stored value. |
search |
✅ | |
head |
✅ | |
exists |
✅ | |
copy |
✅ | |
url |
✅ | |
signedUploadUrl |
⚠️ | PUT URL only - maxSize throws (no presigned POST policy). |