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-presignerUsage
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
RustfsAdapterOptions: Cannot read properties of undefined (reading 'ESNext')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). |