A robust and feature-rich Cloudflare R2 storage provider for Strapi v4 & v5 with advanced file naming, validation, compression, and retry mechanisms.
- 🚀 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
npm install strapi-r2-storage
Add the provider configuration to your Strapi project:
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.
// 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
},
},
},
};
// 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',
},
},
},
};
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.com
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.
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 |
// Example: Only allow images under 10MB
naming: {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedMimeTypes: [
'image/jpeg',
'image/png',
'image/webp',
'image/gif'
],
}
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)
// 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',
},
},
},
},
};
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' }
-
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
Enable logging to troubleshoot issues:
providerOptions: {
// ... other config
enableLogging: true,
}
This will output detailed logs for all operations including uploads, deletions, and retries.
- 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
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.