npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

strapi-provider-upload-gcs-x

v5.32.2

Published

(Non-official) Google Cloud Storage Provider for Strapi Upload

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

Install the package from your app root directory:

with npm:

npm install strapi-provider-upload-gcs-x --save

or 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:

  1. In the GCP Console, go to the Create service account key page:
  2. From the Service account list, select New service account
  3. In the Service account name field, enter a name
  4. From the Role list, select Cloud Storage > Storage Admin
  5. Select JSON for Key Type
  6. Click Create. A JSON file that contains your key downloads to your computer
  7. Copy the full content of the downloaded JSON file
  8. Open the Strapi configuration file
  9. Paste it into the serviceAccount field (as string or JSON, 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.js
  • config/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 days

Note: 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 hour

Security Options

maxFileSize (Optional)

Maximum file size in bytes. Files exceeding this limit will be rejected.

  • Default: 104857600 (100 MB)
maxFileSize: 50 * 1024 * 1024; // 50 MB

allowedExtensions (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 minutes

maxConcurrentUploads (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 options

Concurrent 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:unit

Integration Tests

Integration tests for large file uploads and resumable uploads:

yarn test:integration

Note: 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 benchmark

Benchmarks 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:

  1. Open your ServiceAccount JSON file
  2. Copy the full content of the file
  3. Paste it under the serviceAccount variable in plugins.js config file as JSON

Signed URL Generation Issues

Error: Cannot generate signed URLs without service account credentials

This occurs when:

  1. You're running in a non-GCP environment (local development, other cloud providers)
  2. You have publicFiles: false (requiring signed URLs)
  3. You haven't provided explicit serviceAccount credentials

Solutions:

  • For GCP environments: Ensure your service account has proper permissions (Storage Object Admin or Storage Admin role)
  • For non-GCP environments: Provide explicit serviceAccount configuration with client_email and private_key
  • Alternatively: Set publicFiles: true to 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:

  1. Insufficient permissions on the default service account
  2. Missing IAM roles for URL signing

Solutions:

  • Ensure your GCP service account has Storage Object Admin or Storage Admin role
  • Verify that the default service account has signing permissions
  • Consider providing explicit serviceAccount credentials 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):


📄 License

See the MIT License file for licensing information.