Cloudflare R2
The @sobj/r2 adapter connects Sobj to Cloudflare R2 using the S3-compatible API.
It signs requests with AWS Signature Version 4 using the Web Crypto API — no AWS SDK and no external dependencies.
📥 Installation
pnpm add sobj @sobj/r2⚙️ Configuration
Create an R2 client by passing your credentials to r2():
import { sobj } from 'sobj'
import { r2 } from '@sobj/r2'
const storage = sobj(
r2({
accountId: 'YOUR_ACCOUNT_ID',
accessKeyId: 'YOUR_ACCESS_KEY_ID',
secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
bucket: 'YOUR_BUCKET',
})
)🔧 Config options
| Option | Type | Required | Description |
|---|---|---|---|
accountId | string | ✓ | Cloudflare account ID |
accessKeyId | string | ✓ | R2 S3 API access key ID |
secretAccessKey | string | ✓ | R2 S3 API secret access key |
bucket | string | ✓ | R2 bucket name |
sessionToken | string | – | Temporary credential session token |
endpoint | string | – | Override the S3 endpoint (e.g. for jurisdictional buckets) |
fetch | typeof fetch | – | Custom fetch implementation |
🌐 Jurisdictional buckets
For jurisdictional R2 buckets, override the endpoint:
r2({
// ...
endpoint: 'https://YOUR_ACCOUNT_ID.eu.r2.cloudflarestorage.com',
})📊 Capabilities
The R2 adapter supports all Sobj operations:
| Operation | Supported |
|---|---|
put | ✓ |
get | ✓ |
head | ✓ |
delete | ✓ |
list | ✓ |
copy | ✓ |
presign | ✓ |
multipart | ✓ |
versioning | – |
⚡ Operations
📤 put
Upload an object:
await storage.put('images/avatar.png', body, {
contentType: 'image/png',
metadata: { userId: '123' },
})The body can be a string, Uint8Array, ArrayBuffer, Blob, or ReadableStream<Uint8Array>.
📥 get
Retrieve an object. Returns null if not found.
const object = await storage.get('images/avatar.png')
if (object) {
const text = await new Response(object.body).text()
}With a byte range:
const object = await storage.get('video.mp4', {
range: { start: 0, end: 1_048_575 }, // first 1 MB
})🔍 head
Retrieve metadata without downloading the object body:
const metadata = await storage.head('images/avatar.png')
if (metadata) {
console.log(metadata.size, metadata.contentType, metadata.etag)
}🗑️ delete
Delete an object:
await storage.delete('images/avatar.png')📋 list
List objects in the bucket:
const result = await storage.list()
for (const object of result.objects) {
console.log(object.key)
}With options:
const result = await storage.list({
prefix: 'images/',
delimiter: '/',
limit: 100,
cursor: result.cursor, // for pagination
})The prefixes array contains common prefixes (virtual directories) when a delimiter is used.
📂 copy
Copy an object within the same bucket:
await storage.copy('source/file.txt', 'destination/file.txt')Prevent overwriting an existing destination:
await storage.copy('source.txt', 'dest.txt', { overwrite: false })🔑 presign
Generate a pre-signed URL for GET or PUT:
const url = await storage.presign.get('images/avatar.png', {
expiresIn: 3600, // seconds (default: 3600)
})Pre-signed upload URL:
const url = await storage.presign.put('uploads/file.bin', {
expiresIn: 900,
contentType: 'application/octet-stream',
})📦 multipart
Upload large objects using the S3 multipart upload API:
const upload = await storage.multipart.create('large-file.bin')
const part1 = await storage.multipart.uploadPart(upload, 1, chunk1)
const part2 = await storage.multipart.uploadPart(upload, 2, chunk2)
await storage.multipart.complete(upload, [part1, part2])Abort an incomplete upload:
await storage.multipart.abort(upload)⚠️ Error handling
The adapter throws SobjError subclasses:
import { isSobjError, SobjNotFoundError } from 'sobj/errors'
try {
const object = await storage.get('missing.txt')
} catch (error) {
if (error instanceof SobjNotFoundError) {
console.log('Object not found')
} else if (isSobjError(error)) {
console.log(error.code, error.status)
}
}See the Errors reference for all available error classes.