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 ​

bash
pnpm add sobj @sobj/r2

⚙️ Configuration ​

Create an R2 client by passing your credentials to r2():

ts
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 ​

OptionTypeRequiredDescription
accountIdstring✓Cloudflare account ID
accessKeyIdstring✓R2 S3 API access key ID
secretAccessKeystring✓R2 S3 API secret access key
bucketstring✓R2 bucket name
sessionTokenstring–Temporary credential session token
endpointstring–Override the S3 endpoint (e.g. for jurisdictional buckets)
fetchtypeof fetch–Custom fetch implementation

🌐 Jurisdictional buckets ​

For jurisdictional R2 buckets, override the endpoint:

ts
r2({
  // ...
  endpoint: 'https://YOUR_ACCOUNT_ID.eu.r2.cloudflarestorage.com',
})

📊 Capabilities ​

The R2 adapter supports all Sobj operations:

OperationSupported
put✓
get✓
head✓
delete✓
list✓
copy✓
presign✓
multipart✓
versioning–

⚡ Operations ​

📤 put ​

Upload an object:

ts
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.

ts
const object = await storage.get('images/avatar.png')

if (object) {
  const text = await new Response(object.body).text()
}

With a byte range:

ts
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:

ts
const metadata = await storage.head('images/avatar.png')

if (metadata) {
  console.log(metadata.size, metadata.contentType, metadata.etag)
}

🗑️ delete ​

Delete an object:

ts
await storage.delete('images/avatar.png')

📋 list ​

List objects in the bucket:

ts
const result = await storage.list()

for (const object of result.objects) {
  console.log(object.key)
}

With options:

ts
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:

ts
await storage.copy('source/file.txt', 'destination/file.txt')

Prevent overwriting an existing destination:

ts
await storage.copy('source.txt', 'dest.txt', { overwrite: false })

🔑 presign ​

Generate a pre-signed URL for GET or PUT:

ts
const url = await storage.presign.get('images/avatar.png', {
  expiresIn: 3600, // seconds (default: 3600)
})

Pre-signed upload URL:

ts
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:

ts
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:

ts
await storage.multipart.abort(upload)

⚠️ Error handling ​

The adapter throws SobjError subclasses:

ts
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.