Cloudflare R2 Storage Adapter
The @zintrust/storage-r2 package provides a Cloudflare R2 driver for ZinTrust's storage system, enabling S3-compatible storage with Cloudflare's edge network.
Installation
bash
npm install @zintrust/storage-r2Configuration
Add the R2 storage configuration to your environment:
typescript
// config/storage.ts
import { StorageConfig } from '@zintrust/core';
export const storage: StorageConfig = {
driver: 'r2',
r2: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID,
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY,
bucket: process.env.CLOUDFLARE_R2_BUCKET,
endpoint: process.env.CLOUDFLARE_R2_ENDPOINT || 'https://your-account-id.r2.cloudflarestorage.com',
region: 'auto',
},
};Environment Variables
bash
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_R2_ACCESS_KEY_ID=your-access-key-id
CLOUDFLARE_R2_SECRET_ACCESS_KEY=your-secret-access-key
CLOUDFLARE_R2_BUCKET=your-bucket-name
CLOUDFLARE_R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.comUsage
typescript
import { Storage } from '@zintrust/core';
// Upload a file
const uploadedFile = await Storage.upload('documents/report.pdf', fileBuffer, {
contentType: 'application/pdf',
metadata: {
originalName: 'annual-report.pdf',
uploadedBy: 'user-123',
},
});
// Get file URL
const url = Storage.url('documents/report.pdf');
// Download a file
const fileBuffer = await Storage.download('documents/report.pdf');
// Check if file exists
const exists = await Storage.exists('documents/report.pdf');
// Delete a file
await Storage.delete('documents/report.pdf');
// List files
const files = await Storage.list('documents/', { recursive: true });Features
- S3 Compatible: Full S3 API compatibility
- Edge Network: Global edge network distribution
- Zero Egress Fees: No data transfer fees
- High Performance: Low latency access worldwide
- Auto-scaling: Automatic scaling with usage
- Security: Built-in security features
- Developer Tools: Rich developer ecosystem
- Cost Predictable: Simple, predictable pricing
Advanced Configuration
Custom Endpoint
typescript
export const storage: StorageConfig = {
driver: 'r2',
r2: {
accountId: 'your-account-id',
accessKeyId: 'your-access-key',
secretAccessKey: 'your-secret-key',
bucket: 'your-bucket',
endpoint: 'https://your-account-id.r2.cloudflarestorage.com',
// For testing with MinIO or other S3-compatible services
// endpoint: 'http://localhost:9000',
forcePathStyle: true,
},
};Multiple Buckets
typescript
export const storage: StorageConfig = {
driver: 'r2',
r2: {
accountId: 'your-account-id',
accessKeyId: 'your-access-key',
secretAccessKey: 'your-secret-key',
buckets: {
public: 'public-assets',
private: 'private-files',
uploads: 'user-uploads',
},
defaultBucket: 'public-assets',
},
};Client Configuration
typescript
export const storage: StorageConfig = {
driver: 'r2',
r2: {
// ... other config
clientConfig: {
maxRetries: 5,
retryDelayOptions: {
customBackoff: (retryCount) => Math.pow(2, retryCount) * 100,
},
httpOptions: {
timeout: 30000,
connectTimeout: 5000,
},
},
},
};Bucket Operations
Create Bucket
typescript
import { R2Manager } from '@zintrust/storage-r2';
const manager = new R2Manager();
// Create bucket
await manager.createBucket('my-new-bucket', {
location: 'WEUR', // Western Europe
locationConstraint: 'WEUR',
});
// List buckets
const buckets = await manager.listBuckets();
// Returns: Array<{ name: string, creationDate: Date }>Configure Bucket
typescript
// Enable public access
await manager.setPublicAccess('my-bucket', true);
// Set CORS configuration
await manager.setCORS('my-bucket', [
{
allowedOrigins: ['https://example.com'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['*'],
maxAgeSeconds: 3600,
},
]);
// Set bucket policies
await manager.setPolicy('my-bucket', {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: '*' },
Action: ['s3:GetObject'],
Resource: ['arn:aws:s3:::my-bucket/public/*'],
},
],
});File Operations
Upload with Options
typescript
// Upload with custom headers
await Storage.upload('documents/report.pdf', buffer, {
contentType: 'application/pdf',
cacheControl: 'public, max-age=31536000',
contentEncoding: 'gzip',
metadata: {
originalName: 'annual-report.pdf',
uploadedBy: 'user-123',
category: 'reports',
},
});
// Multipart upload for large files
const uploadId = await Storage.createMultipartUpload('large-file.zip');
const parts = [];
const chunkSize = 8 * 1024 * 1024; // 8MB chunks
for (let i = 0; i < fileBuffer.length; i += chunkSize) {
const part = await Storage.uploadPart(uploadId, i + 1,
fileBuffer.slice(i, i + chunkSize));
parts.push(part);
}
await Storage.completeMultipartUpload(uploadId, parts);Download Options
typescript
// Download with range
const partialBuffer = await Storage.download('large-file.zip', {
range: { start: 0, end: 1024 * 1024 }, // First 1MB
});
// Download with conditional requests
const buffer = await Storage.download('file.pdf', {
ifModifiedSince: new Date('2024-01-01'),
ifNoneMatch: 'etag-value',
});File Management
typescript
// Copy file
await Storage.copy('source/file.pdf', 'backup/file.pdf');
// Move file
await Storage.move('temp/file.pdf', 'final/file.pdf');
// Get file metadata
const metadata = await Storage.getMetadata('documents/report.pdf');
// Returns: { size: number, lastModified: Date, contentType: string, etag: string, metadata: object }
// Update metadata
await Storage.updateMetadata('documents/report.pdf', {
category: 'important',
reviewed: 'true',
});Advanced Features
Signed URLs
typescript
// Generate signed URL for upload
const uploadUrl = await Storage.signedUploadUrl('uploads/', {
expiresIn: 3600, // 1 hour
key: 'user-uploads/file.pdf',
contentType: 'application/pdf',
contentLength: 5 * 1024 * 1024, // 5MB
metadata: { uploadedBy: 'user-123' },
});
// Generate signed URL for download
const downloadUrl = await Storage.signedUrl('private/document.pdf', {
expiresIn: 1800, // 30 minutes
responseDisposition: 'attachment; filename="document.pdf"',
responseContentType: 'application/pdf',
});
// Generate signed URL with conditions
const conditionalUrl = await Storage.signedUrl('uploads/', {
expiresIn: 3600,
conditions: [
{ acl: 'public-read' },
{ 'content-type': 'image/jpeg' },
['content-length-range', 0, 5 * 1024 * 1024], // Max 5MB
],
});Presigned POST
typescript
// Generate presigned POST for browser uploads
const postPolicy = await Storage.presignedPost('uploads/', {
expiresIn: 3600,
conditions: [
{ bucket: 'my-bucket' },
{ key: 'uploads/${filename}' },
{ acl: 'public-read' },
['starts-with', '$Content-Type', 'image/'],
['content-length-range', 0, 10 * 1024 * 1024], // Max 10MB
],
});
// Returns form data for browser uploads
console.log(postPolicy.url);
console.log(postPolicy.fields);Batch Operations
typescript
import { R2Batch } from '@zintrust/storage-r2';
const batch = new R2Batch();
// Add operations to batch
batch.delete('files/old-file1.pdf');
batch.delete('files/old-file2.pdf');
batch.copy('files/current.pdf', 'archive/current-backup.pdf');
// Execute batch
const results = await batch.execute();
// Returns: Array<{ success: boolean, error?: string }>Performance Optimization
Parallel Uploads
typescript
import { parallelUpload } from '@zintrust/storage-r2';
// Upload large file in parallel
await parallelUpload('large-file.zip', fileBuffer, {
chunkSize: 16 * 1024 * 1024, // 16MB chunks
concurrency: 4,
});Edge Caching
typescript
// Set cache headers for edge caching
await Storage.upload('static/style.css', cssBuffer, {
cacheControl: 'public, max-age=31536000, immutable',
});
// Set custom cache headers
await Storage.upload('images/photo.jpg', imageBuffer, {
cacheControl: 'public, max-age=86400',
expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
});Compression
typescript
// Compress uploads
await Storage.upload('data.json', jsonBuffer, {
contentEncoding: 'gzip',
contentType: 'application/json',
cacheControl: 'public, max-age=3600',
});Security
Access Control
typescript
// Set bucket policy for public access
await manager.setPolicy('public-bucket', {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: '*' },
Action: ['s3:GetObject'],
Resource: ['arn:aws:s3:::public-bucket/*'],
},
],
});
// Set private bucket policy
await manager.setPolicy('private-bucket', {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: 'arn:aws:iam::account:user/app-user' },
Action: ['s3:*'],
Resource: ['arn:aws:s3:::private-bucket/*'],
},
],
});CORS Configuration
typescript
await manager.setCORS('my-bucket', [
{
allowedOrigins: ['https://example.com', 'https://app.example.com'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
maxAgeSeconds: 86400,
exposeHeaders: ['ETag', 'Content-Length'],
},
]);Encryption
typescript
// Server-side encryption
await Storage.upload('secure/document.pdf', buffer, {
serverSideEncryption: 'AES256',
});
// Customer-provided encryption keys
await Storage.upload('encrypted/file.pdf', buffer, {
sseCustomerKey: 'my-base64-encoded-encryption-key',
sseCustomerAlgorithm: 'AES256',
});Monitoring and Metrics
Usage Metrics
typescript
import { R2Monitoring } from '@zintrust/storage-r2';
const monitoring = new R2Monitoring();
// Get bucket metrics
const metrics = await monitoring.getBucketMetrics('my-bucket');
// Returns: { storageSize: number, objectCount: number, classAOperations: number, classBOperations: number }
// Get account metrics
const accountMetrics = await monitoring.getAccountMetrics();
// Returns: { totalStorage: number, totalOperations: number, costEstimate: number }Analytics
typescript
// Get popular files
const popularFiles = await monitoring.getPopularFiles('my-bucket', {
period: '7d',
limit: 10,
});
// Get usage by region
const regionalUsage = await monitoring.getRegionalUsage('my-bucket');Error Handling
Retry Configuration
typescript
export const storage: StorageConfig = {
driver: 'r2',
r2: {
// ... other config
retryOptions: {
maxRetries: 5,
retryDelay: 1000,
maxRetryDelay: 30000,
retryableErrorCodes: [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND',
'EAI_AGAIN',
'RequestTimeout',
],
},
},
};Error Types
typescript
try {
await Storage.upload('file.pdf', buffer);
} catch (error) {
if (error.code === 'AccessDenied') {
console.log('Access denied - check credentials');
} else if (error.code === 'NoSuchBucket') {
console.log('Bucket does not exist');
} else if (error.code === 'RequestTimeout') {
console.log('Request timeout - retrying');
} else {
console.log('R2 error:', error.message);
}
}Testing
Local Testing with MinIO
typescript
// Use MinIO for local testing
export const storage: StorageConfig = {
driver: 'r2',
r2: {
accountId: 'test-account',
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
bucket: 'test-bucket',
endpoint: 'http://localhost:9000',
forcePathStyle: true,
region: 'us-east-1',
},
};Mock R2
typescript
import { R2Mock } from '@zintrust/storage-r2';
// Use mock for testing
const mockR2 = new R2Mock();
// Mock operations
mockR2.on('upload', (bucket, key, data) => {
console.log('Mock upload:', bucket, key, data.length);
});
// Test file operations
await mockR2.upload('test-bucket', 'test.txt', Buffer.from('test'));
const exists = await mockR2.exists('test-bucket', 'test.txt');
expect(exists).toBe(true);Integration with Cloudflare Services
Workers Integration
typescript
// Use R2 in Cloudflare Workers
export default {
async fetch(request, env, ctx) {
if (request.method === 'PUT') {
const file = await request.arrayBuffer();
await env.MY_BUCKET.put('file.pdf', file);
return new Response('File uploaded');
}
if (request.method === 'GET') {
const file = await env.MY_BUCKET.get('file.pdf');
return new Response(file.body);
}
},
};Pages Integration
typescript
// Use R2 in Cloudflare Pages
export async function onRequestPost(context) {
const { request, env } = context;
const formData = await request.formData();
const file = formData.get('file');
await env.MY_BUCKET.put(`uploads/${file.name}`, file);
return new Response('File uploaded successfully');
}Best Practices
- Use Appropriate Cache Headers: Set cache headers for static assets
- Implement Lifecycle Policies: Automate data management
- Use Compression: Compress text-based files
- Optimize Uploads: Use multipart uploads for large files
- Monitor Usage: Track storage usage and costs
- Implement Security: Use proper access controls
- Edge Optimization: Leverage Cloudflare's edge network
- Error Handling: Implement robust error handling
Limitations
- Object Size: Maximum 5GB per object (for single upload)
- Multipart Upload: Maximum 5TB per object
- Bucket Count: Limited by account limits
- API Rate Limits: Rate limits apply to R2 API
- Naming Restrictions: Bucket names must be globally unique
- Regional Availability: Some features may be region-specific
Cost Optimization
Storage Classes
typescript
// R2 doesn't have storage classes like S3, but you can implement lifecycle policies
await manager.setLifecycleRules('my-bucket', [
{
action: { type: 'Delete' },
condition: { age: 365 }, // Delete after 1 year
},
]);Usage Monitoring
typescript
// Monitor costs
const costAnalysis = await monitoring.getCostAnalysis('my-bucket');
// Returns: { storageCost: number, operationCost: number, totalCost: number }
// Set up alerts for high usage
await monitoring.setUsageAlert('my-bucket', {
storageThreshold: 100 * 1024 * 1024 * 1024, // 100GB
operationThreshold: 1000000, // 1M operations
});