Skip to content

Google Cloud Storage Driver ​

The GCS driver provides integration with Google Cloud Storage using the @google-cloud/storage SDK. It supports all standard FilesystemContract operations plus GCS-specific features like resumable uploads, uniform bucket-level access, and generation-based concurrency.

Installation ​

bash
pnpm add @google-cloud/storage

Configuration ​

FieldTypeRequiredDefaultDescription
driver'gcs'Yes—Must be 'gcs'
bucketstringYes—GCS bucket name
projectIdstringYes¹—Google Cloud project ID
keyFilenamestringNo—Path to service account JSON key file
credentialsobjectNo—Inline service account credentials (JSON object)
urlstringNo—Custom base URL for public file URLs
visibility'public' | 'private'No'private'Default visibility for new objects

¹ When using Application Default Credentials (ADC) in GKE or Cloud Run, projectId is automatically detected from the environment.

Basic Setup ​

typescript
import { Module } from '@nestjs/common';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    StorageModule.forRoot({
      default: 'gcs',
      disks: {
        gcs: {
          driver: 'gcs',
          bucket: 'my-app-uploads',
          projectId: 'my-gcp-project',
          keyFilename: '/path/to/service-account.json',
          visibility: 'private',
        },
      },
    }),
  ],
})
export class AppModule {}

Async Configuration with ConfigService ​

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    ConfigModule.forRoot(),
    StorageModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        default: 'gcs',
        disks: {
          gcs: {
            driver: 'gcs',
            bucket: config.getOrThrow('GCS_BUCKET'),
            projectId: config.getOrThrow('GCP_PROJECT_ID'),
            keyFilename: config.get('GCS_KEY_FILE'),
            visibility: 'private',
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

Authentication Methods ​

GCS supports three authentication methods, listed from most to least recommended for production:

1. Application Default Credentials (ADC) ​

In GKE, Cloud Run, Cloud Functions, and Compute Engine, credentials are provided automatically by the environment. Omit keyFilename and credentials:

typescript
{
  driver: 'gcs',
  bucket: 'my-app-uploads',
  projectId: 'my-gcp-project',
  // No keyFilename or credentials — ADC is used automatically
}

Application Default Credentials in GKE

When running in GKE, attach a Google Service Account to your Kubernetes Service Account using Workload Identity. The GCS SDK will automatically pick up the credentials. This is the most secure approach — no key files or secrets to manage.

yaml
# k8s service account annotation for Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  annotations:
    iam.gke.io/gcp-service-account: storage-sa@my-project.iam.gserviceaccount.com

2. Service Account Key File ​

Download a JSON key file from the GCP Console and reference it by path:

typescript
{
  driver: 'gcs',
  bucket: 'my-app-uploads',
  projectId: 'my-gcp-project',
  keyFilename: '/etc/secrets/gcs-service-account.json',
}

WARNING

Service account key files are long-lived credentials. If compromised, they grant full access until revoked. Prefer Workload Identity (ADC) in GCP environments. If you must use key files, store them securely and rotate them regularly.

3. Inline Credentials ​

Pass the service account JSON directly as an object (useful for environment variables or secrets managers):

typescript
{
  driver: 'gcs',
  bucket: 'my-app-uploads',
  projectId: 'my-gcp-project',
  credentials: JSON.parse(process.env.GCS_CREDENTIALS),
}
bash
# .env — the entire service account JSON as a single-line string
GCS_CREDENTIALS='{"type":"service_account","project_id":"my-project","private_key_id":"...","private_key":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n","client_email":"storage@my-project.iam.gserviceaccount.com",...}'

GCS-Specific Metadata ​

When you call getMetadata() on a GCS object, you receive extended metadata in GcsFileMetadata:

typescript
const meta = await disk.getMetadata('uploads/report.pdf');

// Standard metadata
console.log(meta.size);         // 2048576
console.log(meta.lastModified); // Date object
console.log(meta.mimeType);     // 'application/pdf'

// GCS-specific metadata
console.log(meta.generation);      // '1710672000000000' — object generation number
console.log(meta.metageneration);  // '1' — metadata generation number
console.log(meta.crc32c);         // 'aabbccdd'
console.log(meta.md5Hash);        // 'base64-encoded-md5'

INFO

Generation numbers are GCS's versioning mechanism. Each time an object is overwritten, it gets a new generation number. The metageneration increments when only the metadata (not the content) is updated.

File Operations ​

typescript
@Injectable()
export class GcsDocumentService {
  constructor(private readonly storage: StorageService) {}

  async uploadDocument(name: string, content: Buffer) {
    const disk = this.storage.disk('gcs');

    // Write a file
    await disk.put(`documents/${name}`, content);

    // Read it back
    const data = await disk.get(`documents/${name}`);

    // Check existence
    const exists = await disk.exists(`documents/${name}`);

    // Get metadata
    const meta = await disk.getMetadata(`documents/${name}`);

    // Public URL
    const url = await disk.url(`documents/${name}`);
    // => https://storage.googleapis.com/my-bucket/documents/report.pdf

    // Copy and move
    await disk.copy(`documents/${name}`, `archive/${name}`);
    await disk.move(`archive/${name}`, `backup/${name}`);

    // Delete
    await disk.delete(`documents/${name}`);

    return { url, size: meta.size };
  }
}

Presigned URLs ​

Generate time-limited signed URLs for private objects:

typescript
@Injectable()
export class GcsSignedUrlService {
  constructor(private readonly storage: StorageService) {}

  async getDownloadUrl(path: string): Promise<string> {
    const disk = this.storage.disk('gcs');

    // URL valid for 30 minutes (1800 seconds)
    const url = await disk.temporaryUrl(path, 1800);

    return url;
  }

  async getUploadUrl(path: string): Promise<string> {
    const disk = this.storage.disk('gcs');

    // Signed URL for client-side PUT upload
    const url = await disk.temporaryUploadUrl(path, 1800, {
      contentType: 'image/png',
    });

    return url;
  }
}

INFO

GCS signed URLs require either a service account key file or inline credentials. ADC alone cannot generate signed URLs because the signing key is not available locally. If you use Workload Identity, consider the IAM signBlob API as an alternative.

Multipart / Resumable Uploads ​

GCS uses resumable uploads for large files. The driver handles this automatically:

typescript
import { createReadStream } from 'fs';

@Injectable()
export class GcsUploadService {
  constructor(private readonly storage: StorageService) {}

  async uploadLargeFile(localPath: string, gcsPath: string) {
    const disk = this.storage.disk('gcs');

    // putFileMultipart uses GCS resumable uploads under the hood
    await disk.putFileMultipart(gcsPath, createReadStream(localPath), {
      partSize: 8 * 1024 * 1024, // 8 MB chunks
      concurrency: 4,
    });
  }

  async uploadWithStream(path: string, stream: Readable) {
    const disk = this.storage.disk('gcs');
    await disk.putStream(path, stream);
  }
}

Manual Multipart Control ​

typescript
const disk = this.storage.disk('gcs');

// Initialize
const uploadId = await disk.initMultipartUpload('large/file.zip');

// Upload parts
const part1 = await disk.putPart(uploadId, 1, chunk1);
const part2 = await disk.putPart(uploadId, 2, chunk2);

// Complete
await disk.completeMultipartUpload(uploadId, [part1, part2]);

Range Requests ​

typescript
// Read a specific byte range
const chunk = await disk.getRange('media/video.mp4', {
  start: 0,
  end: 1048575, // First 1 MB
});

// Serve with automatic range header parsing
@Controller('media')
export class MediaController {
  constructor(private readonly storage: StorageService) {}

  @Get(':filename')
  @RangeServe()
  async serve(@Param('filename') filename: string) {
    return { disk: 'gcs', path: `media/${filename}` };
  }
}

Visibility ​

GCS supports two bucket access control models:

All objects in the bucket share the same access policy. Controlled via IAM:

typescript
// With uniform access, visibility is controlled at the bucket level
// Individual object ACLs are not available
await disk.put('public/image.png', buffer);

Fine-Grained Access ​

Per-object ACLs are available when fine-grained access is enabled on the bucket:

typescript
// Set visibility on upload
await disk.put('public/image.png', buffer, { visibility: 'public' });
await disk.put('private/secret.pdf', buffer, { visibility: 'private' });

// Change visibility
await disk.setVisibility('public/image.png', 'private');

// Check visibility
const vis = await disk.getVisibility('private/secret.pdf');
// => 'private'
VisibilityGCS ACLEffect
'public'publicReadAnyone can access via the object URL
'private'privateOnly authenticated requests with IAM permission

WARNING

Google recommends uniform bucket-level access for new buckets. Fine-grained ACLs are a legacy feature. If your bucket uses uniform access, setVisibility() and getVisibility() will operate based on bucket-level IAM policies rather than per-object ACLs.

Streaming ​

typescript
import { Controller, Get, Res, Param } from '@nestjs/common';
import { Response } from 'express';

@Controller('files')
export class FileController {
  constructor(private readonly storage: StorageService) {}

  @Get(':path(*)')
  async download(@Param('path') path: string, @Res() res: Response) {
    const disk = this.storage.disk('gcs');

    const stream = await disk.getStream(path);
    const meta = await disk.getMetadata(path);

    res.set({
      'Content-Type': meta.mimeType ?? 'application/octet-stream',
      'Content-Length': meta.size?.toString(),
    });

    stream.pipe(res);
  }
}

GcsClientWrapper ​

Access the underlying @google-cloud/storage client for operations not covered by FilesystemContract:

typescript
@Injectable()
export class AdvancedGcsService {
  constructor(private readonly storage: StorageService) {}

  async advancedOperation() {
    const disk = this.storage.disk('gcs');
    const wrapper = disk.getClient() as GcsClientWrapper;
    const bucket = wrapper.bucket;

    // Access the raw @google-cloud/storage Bucket instance
    const [files] = await bucket.getFiles({ prefix: 'logs/', maxResults: 100 });

    // Use GCS-specific features like notifications, HMAC keys, etc.
    const [notifications] = await bucket.getNotifications();
  }
}

Custom Metadata ​

typescript
await disk.put('documents/report.pdf', content, {
  metadata: {
    department: 'engineering',
    author: 'alice',
    reviewStatus: 'approved',
  },
});

const meta = await disk.getMetadata('documents/report.pdf');
console.log(meta.metadata);
// { department: 'engineering', author: 'alice', reviewStatus: 'approved' }

Complete Example: Image Processing Pipeline ​

typescript
import { Injectable, Logger } from '@nestjs/common';
import { StorageService } from '@fozooni/nestjs-storage';
import { v4 as uuid } from 'uuid';

@Injectable()
export class ImagePipelineService {
  private readonly logger = new Logger(ImagePipelineService.name);

  constructor(private readonly storage: StorageService) {}

  private get disk() {
    return this.storage.disk('gcs');
  }

  async processUpload(file: Express.Multer.File) {
    const id = uuid();
    const originalKey = `images/originals/${id}/${file.originalname}`;

    // Upload original
    await this.disk.put(originalKey, file.buffer, {
      metadata: {
        originalName: file.originalname,
        mimeType: file.mimetype,
        uploadedAt: new Date().toISOString(),
      },
    });

    this.logger.log(`Uploaded original: ${originalKey}`);

    // Generate public URL
    const publicUrl = await this.disk.url(originalKey);

    // Generate signed download URL (1 hour)
    const signedUrl = await this.disk.temporaryUrl(originalKey, 3600);

    return {
      id,
      key: originalKey,
      publicUrl,
      signedUrl,
      size: file.size,
      mimeType: file.mimetype,
    };
  }

  async deleteImage(id: string) {
    const prefix = `images/originals/${id}/`;
    const files = await this.disk.listContents(prefix);

    for (const file of files) {
      await this.disk.delete(file.path);
    }

    this.logger.log(`Deleted all files for image ${id}`);
  }
}

Environment Variables Example ​

bash
# .env
GCS_BUCKET=my-app-uploads
GCP_PROJECT_ID=my-gcp-project
GCS_KEY_FILE=/path/to/service-account.json
# Or use inline credentials:
# GCS_CREDENTIALS='{"type":"service_account",...}'

Released under the MIT License.