OPC Stack uses Cloudflare R2 as object storage and it is S3 API compatible.
File path convention
- Public files:
public/*and accessible to everyone - Private files:
private/<userId>/*and only owner can access - Temporary public files:
tmp/public/<userId>/*and accessible to everyone with short cache - Temporary private files:
tmp/private/<userId>/*and only owner can access
Temporary file lifecycle
Temporary files use Cloudflare R2 Object Lifecycle for automatic deletion. Only tmp/public/ and tmp/private/ can be configured:
R2_TMP_LIFECYCLE_RULES=tmp/public/:7;tmp/private/:1
Do not configure retention for public/ or private/. They are persistent object namespaces.
Temporary upload
const response = await fetch('/api/create_r2_tmp_upload_url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
visibility: 'private',
path: 'images/a.png',
content_type: 'image/png',
size: file.size
})
})
const upload = await response.json()
await fetch(upload.upload_url, {
method: 'PUT',
headers: {
'Content-Type': 'image/png'
},
body: file
})
Upload persistent files
import { uploadFile } from '@/r2'
// Upload public file
await uploadFile(env.R2, 'public/logo.png', file, {
contentType: 'image/png'
})
// Upload private file
await uploadFile(env.R2, `private/${userId}/avatar.jpg`, file, {
contentType: 'image/jpeg'
})
Download files
// Get object
const object = await env.R2.get('public/logo.png')
if (!object) {
return ctx.json({ error: 'File not found' }, 404)
}
// Return object
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000'
}
})
List files
// List all files for one user
const list = await env.R2.list({
prefix: `private/${userId}/`
})
const files = list.objects.map(obj => ({
key: obj.key,
size: obj.size,
uploaded: obj.uploaded
}))
Delete files
// Delete single file
await env.R2.delete('public/logo.png')
// Delete multiple files
await env.R2.delete([
'public/file1.png',
'public/file2.png'
])
Permission control
Check file permission in API handlers:
- Public file
public/*: everyone can access - Private file
private/<userId>/*: only owner can access
See src/api/handler/files.ts for reference.
Pre signed URL
R2 does not provide pre signed URL directly. You can generate temporary access token in Worker. See src/api/handler/files.ts.
Image processing
Use Cloudflare Images or process inside Worker.
// Use Cloudflare Images
const response = await fetch(
`https://your-domain.com/cdn-cgi/image/width=200,height=200/${imageUrl}`
)
Frontend upload
// Frontend uploads file to API
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
})
Utility functions
See utilities in src/r2/index.ts:
uploadFilefor uploadgetFileUrlfor file URLisPublicFileto check public pathisUserFileto check user path
Local development
Local development uses Miniflare to emulate R2:
# View local R2 files
ls .wrangler/state/v3/r2/miniflare-R2BucketObject/
FAQ
Q: Is there size limit in R2
Single file max is 5TB. Total storage has no fixed upper limit.
Q: How to implement resumable upload
Use R2 multipart upload API.
Q: How to improve image loading speed
Use Cloudflare Images or CDN cache.