strapi-r2-storage
v1.0.2
Published
Production-ready Cloudflare R2 storage provider for Strapi with advanced features
Maintainers
Readme
strapi-r2-storage
A robust and feature-rich Cloudflare R2 storage provider for Strapi v4 & v5 with advanced file naming, validation, compression, and retry mechanisms.
Features
- 🚀 Strapi v4 & v5 Compatible - Works with both Strapi v4 and v5
- 📁 Simple File Naming - Two reliable strategies (hash, UUID)
- 🔄 Smart File Renaming - Automatic file renaming with original name preservation
- 🗂️ Smart Folder Structure - Organize files by date or custom logic
- 🔒 File Validation - Size limits, MIME type filtering
- 📦 Compression Support - Automatic compression for compatible file types
- 🔄 Retry Logic - Configurable retry with exponential backoff
- 🌐 CDN Support - Works with Cloudflare CDN and custom domains
- 📊 Health Checks - Built-in provider health monitoring
- 🪵 Logging - Comprehensive logging for debugging
- ⚡ TypeScript - Full TypeScript support with type definitions
Installation
npm install strapi-r2-storageConfiguration
Add the provider configuration to your Strapi project:
Strapi Media Library Security Setup
To enable proper thumbnail display in Strapi's Media Library when using external storage providers like R2, you need to configure the Content Security Policy (CSP) to allow loading images from your R2 domain.
Update your config/middlewares.js file:
// config/middlewares.js
module.exports = ({ env }) => [
'strapi::errors',
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': [
"'self'",
'data:',
'blob:',
// Add your R2 domain here
env('CLOUDFLARE_CDN_URL') ? new URL(env('CLOUDFLARE_CDN_URL')).hostname : null,
].filter(Boolean),
'media-src': [
"'self'",
'data:',
'blob:',
// Add your R2 domain here
env('CLOUDFLARE_CDN_URL') ? new URL(env('CLOUDFLARE_CDN_URL')).hostname : null,
].filter(Boolean),
upgradeInsecureRequests: null,
},
},
},
},
];Important: Replace CLOUDFLARE_CDN_URL with your actual environment variable name that contains your R2 public URL.
Basic Configuration
// config/plugins.js
module.exports = {
upload: {
config: {
provider: 'strapi-r2-storage',
providerOptions: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY,
bucket: process.env.CLOUDFLARE_BUCKET,
cdnUrl: process.env.CLOUDFLARE_CDN_URL, // optional
},
},
},
};Advanced Configuration
// config/plugins.js
module.exports = {
upload: {
config: {
provider: 'strapi-r2-storage',
providerOptions: {
// Required settings
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY,
bucket: process.env.CLOUDFLARE_BUCKET,
// Optional settings
region: 'auto', // default: 'auto'
baseUrl: process.env.CLOUDFLARE_BASE_URL, // custom domain
cdnUrl: process.env.CLOUDFLARE_CDN_URL, // CDN URL
publicDomain: process.env.CLOUDFLARE_PUBLIC_DOMAIN, // R2 public domain
// File naming and organization
naming: {
strategy: 'hash', // 'hash' | 'uuid'
preserveExtension: true,
folderStructure: 'year-month', // 'flat' | 'year' | 'year-month' | 'year-month-day'
},
// File validation
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'],
blockedMimeTypes: ['application/x-executable'],
// Performance and caching
enableCompression: true,
cacheControl: 'public, max-age=31536000', // 1 year
enablePublicRead: true,
// Additional metadata
metadata: {
'uploaded-by': 'strapi',
'environment': process.env.NODE_ENV,
},
// Retry configuration
retryOptions: {
maxRetries: 3,
retryDelay: 1000, // ms
},
// Debugging
enableLogging: process.env.NODE_ENV === 'development',
},
},
},
};Environment Variables
Create a .env file in your Strapi project root:
# Cloudflare R2 Configuration
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
CLOUDFLARE_ACCESS_KEY_ID=your_access_key_here
CLOUDFLARE_SECRET_ACCESS_KEY=your_secret_key_here
CLOUDFLARE_BUCKET=your_bucket_name
# Optional: CDN/Custom Domain
CLOUDFLARE_CDN_URL=https://your-cdn-domain.com
CLOUDFLARE_BASE_URL=https://your-custom-domain.comConfiguration Options
File Naming Strategies
| Strategy | Description | Example Output | Best For |
|----------|-------------|----------------|----------|
| hash | Uses Strapi's generated hash (default) | a1b2c3d4e5f6.jpg | Standard usage, consistent with Strapi defaults |
| uuid | Generated UUID v4 | 550e8400-e29b-41d4-a716-446655440000.jpg | Complete uniqueness, no hash conflicts |
Recommendation: Use uuid for maximum reliability and to avoid any potential file naming issues.
Folder Structures
| Structure | Description | Example Path |
|-----------|-------------|--------------|
| flat | All files in root (default) | file.jpg |
| year | Organized by year | 2024/file.jpg |
| year-month | Year and month | 2024/03/file.jpg |
| year-month-day | Full date | 2024/03/15/file.jpg |
File Validation
// Example: Only allow images under 10MB
naming: {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedMimeTypes: [
'image/jpeg',
'image/png',
'image/webp',
'image/gif'
],
}Strapi v5 Compatibility
This provider is fully compatible with both Strapi v4 and v5. The API includes the new customParams parameter required for v5:
// The provider automatically handles both v4 and v5 API signatures
// v4: upload(file)
// v5: upload(file, customParams)Usage Examples
Development vs Production
// Different settings per environment
const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
upload: {
config: {
provider: 'strapi-r2-storage',
providerOptions: {
// ... other config
enableLogging: isDevelopment,
enableCompression: !isDevelopment, // Disable compression in dev for faster uploads
cacheControl: isDevelopment
? 'no-cache'
: 'public, max-age=31536000',
naming: {
strategy: isDevelopment ? 'hash' : 'uuid',
},
},
},
},
};Health Checks
The provider includes a built-in health check method:
// In your Strapi application
const uploadProvider = strapi.plugins.upload.provider;
const healthCheck = await uploadProvider.checkHealth();
console.log(healthCheck);
// { status: 'ok' } or { status: 'error', message: 'Error details' }Troubleshooting
Common Issues
Authentication Errors
- Verify your Cloudflare credentials
- Ensure the API token has R2 permissions
Upload Failures
- Check file size limits
- Verify MIME type restrictions
- Review bucket permissions
URL Generation Issues
- Confirm CDN URL configuration
- Check bucket public access settings
Files Not Accessible (Cannot Copy/Download)
- Problem: Generated URLs return errors or are not accessible
- Solution: Configure proper public access
// Option 1: Use custom domain (Recommended) providerOptions: { cdnUrl: 'https://your-domain.com', enablePublicRead: true, } // Option 2: Use R2 public domain providerOptions: { publicDomain: 'https://pub-abc123.r2.dev', // Your actual R2 public domain enablePublicRead: true, } // Option 3: For private files, ensure signed URLs work providerOptions: { enablePublicRead: false, // Will generate signed URLs }R2 Bucket Configuration
- In Cloudflare dashboard, ensure your R2 bucket has public access enabled if using
enablePublicRead: true - Set up custom domain in R2 settings for better URL structure
- In Cloudflare dashboard, ensure your R2 bucket has public access enabled if using
Debug Mode
Enable logging to troubleshoot issues:
providerOptions: {
// ... other config
enableLogging: true,
}This will output detailed logs for all operations including uploads, deletions, and retries.
Performance Tips
- Enable Compression: Reduces bandwidth for text-based files
- Use CDN URLs: Faster content delivery worldwide
- Configure Caching: Set appropriate cache headers
- Optimize Retry Settings: Balance reliability with performance
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
