strapi-provider-upload-gcs-x
v5.32.2
Published
(Non-official) Google Cloud Storage Provider for Strapi Upload
Maintainers
Readme
🚀 What's New (2025 Refactor)
This library has been completely refactored from JavaScript to TypeScript with enterprise-grade improvements:
- 🔒 Security-First: File size validation, path sanitization, extension/MIME type validation, credential masking, and secure signed URL policies
- ⚡ High Performance: Resumable uploads for large files, automatic buffer-to-stream conversion, connection pooling, concurrent upload queue, and bucket existence caching
- 🛡️ Production Resilience: Exponential backoff retry logic, operation timeouts, circuit breaker pattern, health checks, and progress tracking
- 📚 Full TypeScript: Strict type safety, comprehensive JSDoc documentation, and Zod schema validation
- 🧪 Testing & Benchmarks: Integration tests for large file uploads and performance benchmarking tools
All enhancements are opt-in and fully backward compatible with existing Strapi projects.
📋 Table of Contents
- Installation
- Quick Start
- Configuration Reference
- Security Features
- Performance Features
- Advanced Features
- Testing & Benchmarks
- FAQ & Troubleshooting
- Community Support
- License
📦 Installation
Install the package from your app root directory:
with npm:
npm install strapi-provider-upload-gcs-x --saveor with yarn:
yarn add strapi-provider-upload-gcs-x🪣 Create Your Bucket on Google Cloud Storage
The bucket should be created with fine-grained access control, as the plugin will configure uploaded files with public read access.
- How to create a bucket: https://cloud.google.com/storage/docs/creating-buckets
- Bucket locations: https://cloud.google.com/storage/docs/locations
🔐 Setting up Google Authentication
For GCP Environments (App Engine, Cloud Run, Cloud Functions, GKE, Compute Engine)
If you're deploying to a Google Cloud Platform product that supports Application Default Credentials, you can skip explicit credential configuration. The provider will automatically detect the GCP environment and use ADC.
For Non-GCP Environments
Follow these steps to set up authentication:
- In the GCP Console, go to the Create service account key page:
- From the Service account list, select New service account
- In the Service account name field, enter a name
- From the Role list, select Cloud Storage > Storage Admin
- Select
JSONfor Key Type - Click Create. A JSON file that contains your key downloads to your computer
- Copy the full content of the downloaded JSON file
- Open the Strapi configuration file
- Paste it into the
serviceAccountfield (asstringorJSON, be careful with indentation)
⚙️ Quick Start
Minimal Setup (GCP Environments)
For GCP deployments using Application Default Credentials:
// config/plugins.js
module.exports = {
upload: {
config: {
provider: 'strapi-provider-upload-gcs-x',
providerOptions: {
bucketName: 'your-bucket-name',
publicFiles: false,
uniform: false,
basePath: '',
},
},
},
};With Service Account (Non-GCP Environments)
// config/plugins.js
module.exports = {
upload: {
config: {
provider: 'strapi-provider-upload-gcs-x',
providerOptions: {
bucketName: 'your-bucket-name',
publicFiles: true,
uniform: false,
serviceAccount: {
project_id: 'your-project-id',
client_email: '[email protected]',
private_key:
'-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n',
},
baseUrl: 'https://storage.googleapis.com/your-bucket-name',
basePath: '',
},
},
},
};With Environment Variables
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'strapi-provider-upload-gcs-x',
providerOptions: {
serviceAccount: env.json('GCS_SERVICE_ACCOUNT'),
bucketName: env('GCS_BUCKET_NAME'),
basePath: env('GCS_BASE_PATH'),
baseUrl: env('GCS_BASE_URL'),
publicFiles: env.bool('GCS_PUBLIC_FILES', true),
uniform: env.bool('GCS_UNIFORM', false),
},
},
sizeLimit: 100 * 1024 * 1024, // 100MB (optional)
// Add security configuration to remove the warning
security: {
// You can also configure other security options here
allowedTypes: env.array('ALLOWED_MIMETYPES'),
deniedTypes: env.array('DENIED_MIMETYPES'),
},
},
});Upload Security Configuration
To remove the warning about missing upload security configuration, add the security section to your upload plugin config:
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'strapi-provider-upload-gcs-x',
providerOptions: {
// ... your provider options ...
},
},
// Security configuration (recommended)
security: {
// This is Strapi's additional layer of validation
},
},
});Environment-Specific Configuration
You can override the configuration per environment:
config/env/development/plugins.jsconfig/env/production/plugins.js
Files under config/env/{env}/ will override the default configuration in the main config folder.
Complete Configuration Example
Here's a comprehensive example showing all possible providerOptions:
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'strapi-provider-upload-gcs-x',
providerOptions: {
// ============================================
// REQUIRED OPTIONS
// ============================================
bucketName: 'my-strapi-bucket', // Required: GCS bucket name
// ============================================
// AUTHENTICATION (Optional)
// ============================================
// Can be omitted in GCP environments (uses ADC)
// Can be provided as object or JSON string
serviceAccount: {
project_id: 'your-project-id',
client_email: '[email protected]',
private_key:
'-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n',
},
// OR as JSON string:
// serviceAccount: env.json('GCS_SERVICE_ACCOUNT'),
// ============================================
// CORE OPTIONS
// ============================================
baseUrl: 'https://storage.googleapis.com/my-strapi-bucket',
// OR with placeholder:
// baseUrl: 'https://storage.googleapis.com/{bucket-name}',
// OR custom CDN:
// baseUrl: 'https://cdn.yourdomain.com',
basePath: 'uploads', // Base path for uploaded files (default: '')
publicFiles: true, // Set to false for private files (default: true)
uniform: false, // Set to true if uniform bucket-level access is enabled (default: false)
skipCheckBucket: false, // Skip bucket existence check (default: false)
// ============================================
// CACHING & COMPRESSION
// ============================================
cacheMaxAge: 3600, // Cache control max-age in seconds (default: 3600 = 1 hour)
gzip: 'auto', // Compression: 'auto', true, or false (default: 'auto')
// ============================================
// SIGNED URL OPTIONS
// ============================================
expires: 15 * 60 * 1000, // Signed URL expiration in milliseconds
// Options: number (ms), Date object, or string
// Default: 900000 (15 minutes)
// Min: 60000 (1 minute), Max: 604800000 (7 days)
// ============================================
// SECURITY OPTIONS
// ============================================
maxFileSize: 100 * 1024 * 1024, // Maximum file size in bytes (default: 100MB)
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4', 'mov'], // Optional: restrict file types
// If not provided, all extensions are allowed
// ============================================
// PERFORMANCE OPTIONS
// ============================================
uploadTimeout: 300000, // Upload timeout in milliseconds (default: 5 minutes)
maxRetries: 3, // Maximum retry attempts (default: 3, range: 0-10)
maxConcurrentUploads: 10, // Max concurrent uploads (default: 10)
// ============================================
// CIRCUIT BREAKER (Advanced)
// ============================================
enableCircuitBreaker: false, // Enable circuit breaker pattern (default: false)
circuitBreakerThreshold: 5, // Failures before opening circuit (default: 5)
circuitBreakerTimeout: 60000, // Time before retry in ms (default: 60000 = 1 minute)
// ============================================
// CUSTOM FUNCTIONS (Advanced)
// ============================================
// Custom metadata function
metadata: (file) => ({
cacheControl: `public, max-age=${7 * 24 * 60 * 60}`, // 7 days
contentDisposition: `inline; filename="${file.name}"`,
contentLanguage: 'en-US',
// See: https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request_properties_JSON
}),
// Custom content type function
getContentType: (file) => {
// Custom logic to determine content type
if (file.ext === '.csv') {
return 'text/csv';
}
return file.mime; // Default: use file.mime
},
// Custom file name generation
generateUploadFileName: async (basePath, file) => {
// Custom logic for file naming
const timestamp = Date.now();
const extension = file.ext?.toLowerCase() || '';
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
return `${basePath}/${timestamp}-${sanitizedName}${extension}`;
},
// Upload progress tracking
onUploadProgress: (bytesUploaded, totalBytes) => {
const percentage = ((bytesUploaded / totalBytes) * 100).toFixed(2);
console.log(
`Upload progress: ${percentage}% (${bytesUploaded}/${totalBytes} bytes)`,
);
// You can emit events, update UI, etc.
},
},
},
},
});Note: Most options have sensible defaults. You only need to specify options you want to customize. The minimum required configuration is:
providerOptions: {
bucketName: 'my-strapi-bucket',
// All other options use defaults
}📝 Configuration Reference
Core Options
bucketName (Required)
The name of the bucket on Google Cloud Storage.
bucketName: 'my-strapi-bucket';serviceAccount (Optional)
Service account credentials for authentication. Can be omitted in GCP environments using Application Default Credentials.
Behavior:
- GCP environment + no serviceAccount: Uses ADC for signed URLs ✅
- GCP environment + explicit serviceAccount: Uses provided credentials ✅
- Non-GCP environment + no serviceAccount + publicFiles: true: Returns direct URLs with warning ⚠️
- Non-GCP environment + no serviceAccount + publicFiles: false: Throws error ❌
- Non-GCP environment + explicit serviceAccount: Uses provided credentials ✅
Can be set as a String, JSON Object, or omitted.
Example:
serviceAccount: {
project_id: 'your-project-id',
client_email: '[email protected]',
private_key: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n',
}baseUrl (Optional)
Define your base URL. Default value: https://storage.googleapis.com/{bucket-name}
baseUrl: 'https://storage.googleapis.com/your-bucket-name';
// or
baseUrl: 'https://your-bucket-name.storage.googleapis.com';
// or custom CDN
baseUrl: 'https://cdn.yourdomain.com';basePath (Optional)
Define base path to save each media document.
basePath: 'uploads';publicFiles (Optional)
Boolean to define public attribute for files when uploading to storage.
- Default:
true - When
false: Files are signed and only visible to authenticated users in Content Manager (not Content API)
publicFiles: false;uniform (Optional)
Boolean to define uniform bucket-level access. Set to true when uniform bucket-level access is enabled on your bucket.
- Default:
false
uniform: true;skipCheckBucket (Optional)
Boolean to skip bucket existence check. Useful for private buckets where the check might fail due to permissions.
- Default:
false
skipCheckBucket: true;cacheMaxAge (Optional)
Number to set the cache-control header for uploaded files in seconds.
- Default:
3600(1 hour)
cacheMaxAge: 604800; // 7 daysNote: If you provide a custom metadata function, the cacheMaxAge option will be ignored. You'll need to handle caching in your custom metadata function.
gzip (Optional)
Value to define if files are uploaded and stored with gzip compression.
- Possible values:
true,false,auto - Default:
auto
gzip: 'auto';expires (Optional)
Expiration time for signed URLs. Files are signed when publicFiles is set to false.
- Possible values:
Date,number(milliseconds),string - Default:
900000(15 minutes) - Minimum:
60000(1 minute) - Maximum:
604800000(7 days)
expires: 3600000; // 1 hourSecurity Options
maxFileSize (Optional)
Maximum file size in bytes. Files exceeding this limit will be rejected.
- Default:
104857600(100 MB)
maxFileSize: 50 * 1024 * 1024; // 50 MBallowedExtensions (Optional)
Array of allowed file extensions (without leading dot). If provided, only files with these extensions will be accepted.
- Default:
undefined(all extensions allowed)
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'mp4'];Performance Options
uploadTimeout (Optional)
Timeout in milliseconds for upload operations. Operations exceeding this timeout will be rejected.
- Default:
300000(5 minutes)
uploadTimeout: 600000; // 10 minutesmaxConcurrentUploads (Optional)
Maximum number of concurrent uploads. Uploads exceeding this limit will be queued.
- Default:
10
maxConcurrentUploads: 5;maxRetries (Optional)
Maximum number of retry attempts for failed operations with exponential backoff.
- Default:
3 - Range:
0-10
maxRetries: 5;Advanced Options
metadata (Optional)
Function that computes metadata for a file when it is uploaded.
When no function is provided, the following metadata is used (using the configured cacheMaxAge value):
{
contentDisposition: `inline; filename="${file.name}"`,
cacheControl: `public, max-age=${cacheMaxAge}`,
}Example:
metadata: (file) => ({
cacheControl: `public, max-age=${60 * 60 * 24 * 7}`, // One week
contentLanguage: 'en-US',
contentDisposition: `attachment; filename="${file.name}"`,
}),The available properties can be found in the Cloud Storage JSON API documentation.
generateUploadFileName (Optional)
Function that generates the name of the uploaded file. This method provides control over file naming and can be used to include custom hashing functions or dynamic paths.
When no function is provided, the default algorithm is used (see src/types.ts).
Example:
generateUploadFileName: async (basePath, file) => {
const hash = await computeHash(file.buffer); // Your hashing function
const extension = file.ext?.toLowerCase().substring(1) || 'bin';
return `${extension}/${slugify(file.name)}-${hash}.${extension}`;
},getContentType (Optional)
Function that determines the content type for a file when it is uploaded.
When no function is provided, file.mime is used.
Important: When a custom getContentType function is provided, the file's MIME type will be updated both in Google Cloud Storage metadata and in the Strapi database to ensure consistency.
Example:
getContentType: (file) => {
if (file.ext === '.csv') {
return 'text/csv';
}
return file.mime; // Fallback to original MIME type
},onUploadProgress (Optional)
Callback function that tracks upload progress. Called with (bytesUploaded, totalBytes).
Example:
onUploadProgress: (uploaded, total) => {
const percentage = ((uploaded / total) * 100).toFixed(2);
console.log(`Upload progress: ${percentage}%`);
},enableCircuitBreaker (Optional)
Enable circuit breaker pattern to prevent cascading failures.
- Default:
false
enableCircuitBreaker: true,
circuitBreakerThreshold: 5, // Number of failures before opening circuit
circuitBreakerTimeout: 60000, // Time in ms before attempting to close circuit🔒 Security Features
This provider includes multiple security enhancements:
File Size Validation
Files exceeding maxFileSize are automatically rejected:
providerOptions: {
maxFileSize: 50 * 1024 * 1024, // 50 MB limit
}Path Sanitization
All file paths are automatically sanitized to prevent path traversal attacks (../ sequences are removed, unsafe characters are replaced).
Extension Allowlist
Restrict uploads to specific file extensions:
providerOptions: {
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'],
}MIME Type Validation
MIME types are validated against file extensions with warnings for mismatches.
Credential Masking
Sensitive credentials are automatically masked in error messages and logs to prevent credential leakage.
Signed URL Security
Signed URLs have enforced expiration limits (minimum 1 minute, maximum 7 days) and are validated before use.
⚡ Performance Features
Resumable Uploads
Large files (>5MB) automatically use resumable uploads for better reliability and recovery from network interruptions.
Buffer-to-Stream Conversion
Large buffers (>10MB) are automatically converted to streams to reduce memory usage.
Connection Pooling
HTTP connections are pooled and reused for better performance:
// Automatically enabled when serviceAccount is provided
// Configurable via Storage optionsConcurrent Upload Queue
Uploads are managed in a queue with configurable concurrency limits:
providerOptions: {
maxConcurrentUploads: 10, // Default
}Bucket Existence Caching
Bucket existence checks are cached for 5 minutes to reduce API calls.
Optimized Delete Flow
File deletions are fire-and-forget (non-blocking) to improve upload performance.
🛡️ Stability Features
Retry Logic with Exponential Backoff
Failed operations are automatically retried with exponential backoff:
providerOptions: {
maxRetries: 3, // Default, with exponential backoff: 1s, 2s, 4s
}Operation Timeouts
All async operations are wrapped with timeouts to prevent hanging:
providerOptions: {
uploadTimeout: 300000, // 5 minutes default
}Circuit Breaker Pattern
Prevent cascading failures with circuit breaker:
providerOptions: {
enableCircuitBreaker: true,
circuitBreakerThreshold: 5,
circuitBreakerTimeout: 60000,
}Health Check
Monitor provider health:
const health = await provider.healthCheck();
// Returns: { status: 'healthy' | 'unhealthy', details: {...} }Error Classification
Errors are automatically classified as retryable (network errors, 5xx, 429) or non-retryable (auth errors, 4xx).
🔒 Setting up strapi::security Middlewares
To avoid CSP blocked URLs, edit ./config/middlewares.js:
module.exports = [
'strapi::errors',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': ["'self'", 'data:', 'blob:', 'storage.googleapis.com'],
'media-src': ["'self'", 'data:', 'blob:', 'storage.googleapis.com'],
upgradeInsecureRequests: null,
},
},
},
},
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::favicon',
'strapi::public',
];Replace storage.googleapis.com with your custom CDN URL if applicable.
🧪 Testing & Benchmarks
Unit Tests
Run unit tests:
yarn test:unitIntegration Tests
Integration tests for large file uploads and resumable uploads:
yarn test:integrationNote: Integration tests require GCS credentials. Set the following environment variables:
export GCS_SERVICE_ACCOUNT='{"project_id":"...","client_email":"...","private_key":"..."}'
export GCS_BUCKET_NAME='your-test-bucket'Performance Benchmarks
Run performance benchmarks:
yarn benchmarkBenchmarks measure:
- Small file uploads (1MB)
- Large file uploads (50MB)
- Stream uploads
- Concurrent uploads
See benchmarks/README.md for details.
❓ FAQ & Troubleshooting
Common Errors
Uniform Access Error
Error: Cannot insert legacy ACL for an object when uniform bucket-level access is enabled
Solution: Set uniform: true in your configuration:
providerOptions: {
uniform: true,
}Service Account JSON Error
Error: Error parsing data "Service Account JSON", please be sure to copy/paste the full JSON file
Solution:
- Open your
ServiceAccountJSON file - Copy the full content of the file
- Paste it under the
serviceAccountvariable inplugins.jsconfig file as JSON
Signed URL Generation Issues
Error: Cannot generate signed URLs without service account credentials
This occurs when:
- You're running in a non-GCP environment (local development, other cloud providers)
- You have
publicFiles: false(requiring signed URLs) - You haven't provided explicit
serviceAccountcredentials
Solutions:
- For GCP environments: Ensure your service account has proper permissions (
Storage Object AdminorStorage Adminrole) - For non-GCP environments: Provide explicit
serviceAccountconfiguration withclient_emailandprivate_key - Alternatively: Set
publicFiles: trueto use direct URLs instead of signed URLs
Error: Failed to generate signed URL in GCP environment
This occurs in GCP environments when Application Default Credentials (ADC) cannot sign URLs, typically due to:
- Insufficient permissions on the default service account
- Missing IAM roles for URL signing
Solutions:
- Ensure your GCP service account has
Storage Object AdminorStorage Adminrole - Verify that the default service account has signing permissions
- Consider providing explicit
serviceAccountcredentials if ADC continues to fail
💬 Community Support
- GitHub (Bug reports, contributions)
You can also use the official support platform of Strapi, and search for [VirtusLab] prefixed people (maintainers):
- Discord (For live discussion with the Community and Strapi team)
- Community Forum (Questions and Discussions)
📄 License
See the MIT License file for licensing information.
