storage-adapters

📁 rytass/utils 📅 8 days ago
4
总安装量
2
周安装量
#50938
全站排名
安装命令
npx skills add https://github.com/rytass/utils --skill storage-adapters

Agent 安装分布

replit 2
amp 2
opencode 2
kimi-cli 2
github-copilot 2

Skill 文档

File Storage Adapters

This skill provides comprehensive guidance for using @rytass/storages-adapter-* packages to integrate file storage providers.

Overview

All adapters implement the StorageInterface from @rytass/storages, providing a unified API across different storage providers:

Package Provider Description
@rytass/storages-adapter-s3 AWS S3 Amazon S3 storage adapter
@rytass/storages-adapter-gcs Google Cloud Storage GCS storage adapter
@rytass/storages-adapter-r2 Cloudflare R2 R2 storage adapter with custom domain support
@rytass/storages-adapter-azure-blob Azure Blob Storage Azure blob storage adapter
@rytass/storages-adapter-local Local File System Local disk storage with usage tracking

Base Interface (@rytass/storages)

All adapters share these core methods:

// StorageInterface - 基礎介面(僅定義核心方法)
interface StorageInterface {
  // Upload files
  write(file: InputFile, options?: WriteFileOptions): Promise<StorageFile>;
  batchWrite(files: InputFile[]): Promise<StorageFile[]>;

  // Download files (3 overloads)
  read(key: string): Promise<Readable>;
  read(key: string, options: ReadBufferFileOptions): Promise<Buffer>;
  read(key: string, options: ReadStreamFileOptions): Promise<Readable>;

  // Delete files
  remove(key: string): Promise<void>;
}

// ReadFileOptions 型別
interface ReadBufferFileOptions {
  format: 'buffer';
}
interface ReadStreamFileOptions {
  format: 'stream';
}

// 以下方法在 Storage 抽象類別中定義(不在 StorageInterface):
// - isExists(key: string): Promise<boolean>;  // 所有 adapter 都支援

// 以下方法為各 adapter 的額外功能(不在 StorageInterface):
// - url(key: string, options?): Promise<string>;  // 僅雲端 adapter 支援

注意: batchWrite() 的 options 陣列參數僅 LocalStorage 支援。雲端適配器 (S3, GCS, R2, Azure Blob) 的 batchWrite() 不接受 options 參數。

Key Types(從 @rytass/storages 導出):

  • InputFile – Buffer or Readable stream (alias for ConvertableFile)
  • StorageFile – Object with readonly key: string property
  • WriteFileOptions{ filename?: string; contentType?: string }
  • FilenameHashAlgorithm'sha1' | 'sha256'
  • FileKey – Type alias for string
  • ReadBufferFileOptions{ format: 'buffer' }
  • ReadStreamFileOptions{ format: 'stream' }
  • ConverterManager – Re-export from @rytass/file-converter

StorageOptions:

interface StorageOptions<O = Record<string, unknown>> {
  converters?: FileConverter<O>[];  // 檔案轉換器陣列
  hashAlgorithm?: FilenameHashAlgorithm;  // 檔名雜湊演算法(預設 'sha256')
}

Storage Base Class:

abstract class Storage<O = Record<string, unknown>> implements StorageInterface {
  readonly converterManager: ConverterManager;  // 檔案轉換管道
  readonly hashAlgorithm: FilenameHashAlgorithm;  // 檔名雜湊演算法

  // 檔案類型偵測輔助方法
  getExtension(file: InputFile): Promise<FileTypeResult | undefined>;
  getBufferFilename(buffer: Buffer): Promise<[string, string | undefined]>;
  getStreamFilename(stream: Readable): Promise<[string, string | undefined]>;

  // 抽象方法(各 adapter 必須實作)
  abstract write(file: InputFile, options?: WriteFileOptions): Promise<StorageFile>;
  abstract batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise<StorageFile[]>;
  abstract read(key: string): Promise<Readable>;
  abstract read(key: string, options: ReadBufferFileOptions): Promise<Buffer>;
  abstract read(key: string, options: ReadStreamFileOptions): Promise<Readable>;
  abstract remove(key: string): Promise<void>;
  abstract isExists(key: string): Promise<boolean>;  // 在 Storage 抽象類別中,不在 StorageInterface
}

Installation

# Install base package
npm install @rytass/storages

# Choose the adapter for your provider
npm install @rytass/storages-adapter-s3
npm install @rytass/storages-adapter-gcs
npm install @rytass/storages-adapter-r2
npm install @rytass/storages-adapter-azure-blob
npm install @rytass/storages-adapter-local

Quick Start

AWS S3

import { StorageS3Service } from '@rytass/storages-adapter-s3';
import { readFileSync, createReadStream } from 'fs';

// Initialize S3 storage
const storage = new StorageS3Service({
  accessKey: process.env.AWS_ACCESS_KEY_ID!,
  secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  bucket: 'my-bucket',
  region: 'ap-northeast-1',
  // endpoint: 'https://custom-s3-endpoint.com', // Optional: custom S3 endpoint (e.g., MinIO)
});

// Upload a Buffer
const buffer = readFileSync('./document.pdf');
const file1 = await storage.write(buffer, {
  filename: 'documents/report.pdf',
  contentType: 'application/pdf',
});
console.log('Uploaded:', file1.key); // documents/report.pdf

// Upload a Stream (auto-generates filename with hash)
const stream = createReadStream('./image.jpg');
const file2 = await storage.write(stream);
console.log('Uploaded:', file2.key); // e.g., a3f2...b1c4.jpg

// Download as Buffer
const downloadedBuffer = await storage.read(file1.key, { format: 'buffer' });

// Download as Stream
const downloadedStream = await storage.read(file1.key, { format: 'stream' });

// Generate presigned URL (valid for limited time)
const url = await storage.url(file1.key);
console.log('Presigned URL:', url);

// Delete file
await storage.remove(file1.key);

// Check if file exists
const exists = await storage.isExists(file1.key);
console.log('Exists:', exists); // false

Google Cloud Storage

import { StorageGCSService } from '@rytass/storages-adapter-gcs';

// Initialize GCS storage with service account credentials
const storage = new StorageGCSService({
  bucket: 'my-gcs-bucket',
  projectId: 'my-project-id',
  credentials: {
    client_email: process.env.GCS_CLIENT_EMAIL!,
    private_key: process.env.GCS_PRIVATE_KEY!.replace(/\\n/g, '\n'),
  },
});

// Upload file
const file = await storage.write(buffer, {
  filename: 'uploads/file.pdf',
});

// Generate signed URL with custom expiration (default: 24 hours)
const url = await storage.url(file.key, Date.now() + 1000 * 60 * 60); // 1 hour

Cloudflare R2

import { StorageR2Service } from '@rytass/storages-adapter-r2';

// Initialize R2 storage with custom domain
const storage = new StorageR2Service({
  accessKey: process.env.R2_ACCESS_KEY!,
  secretKey: process.env.R2_SECRET_KEY!,
  bucket: 'my-r2-bucket',
  account: process.env.R2_ACCOUNT_ID!,
  customDomain: 'https://cdn.example.com', // Optional: rewrites presigned URLs
});

// Upload and get presigned URL
const file = await storage.write(buffer);

// Generate presigned URL with custom expiration (in seconds)
const url = await storage.url(file.key, { expires: 3600 }); // 1 hour
console.log('Custom domain URL:', url); // Uses customDomain if configured

Azure Blob Storage

import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob';

// Initialize Azure Blob storage
const storage = new StorageAzureBlobService({
  connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!,
  container: 'my-container',
});

// Upload file
const file = await storage.write(buffer, {
  filename: 'files/document.pdf',
});

// Generate SAS token URL with custom expiration
const url = await storage.url(file.key, Date.now() + 1000 * 60 * 60 * 24); // 24 hours

Local File System

import { LocalStorage, StorageLocalUsageInfo } from '@rytass/storages-adapter-local';

// Initialize local storage
const storage = new LocalStorage({
  directory: './uploads',
  autoMkdir: true, // Automatically create directory if not exists
});

// Upload file
const file = await storage.write(buffer, {
  filename: 'documents/file.pdf',
});

// Download file
const downloadedBuffer = await storage.read(file.key, { format: 'buffer' });

// Get disk usage information (unique to Local adapter)
// Returns StorageLocalUsageInfo: { used: number, free: number, total: number } (in MB)
const usage: StorageLocalUsageInfo = await storage.getUsageInfo();
console.log(`Used: ${usage.used}MB, Free: ${usage.free}MB, Total: ${usage.total}MB`);

LocalStorage Types:

import {
  LocalStorage,
  StorageLocalOptions,
  StorageLocalUsageInfo,
  StorageLocalHelperCommands,
} from '@rytass/storages-adapter-local';

// Options interface
interface StorageLocalOptions extends StorageOptions {
  directory: string;      // 儲存目錄路徑
  autoMkdir?: boolean;    // 自動建立目錄(預設 false)
}

// Usage info interface (單位: MB)
interface StorageLocalUsageInfo {
  used: number;   // 已使用空間
  free: number;   // 可用空間
  total: number;  // 總空間
}

// Helper commands for *NIX systems (內部使用)
enum StorageLocalHelperCommands {
  USED = "du -sm __DIR__ | awk '{ print $1 }'",
  FREE = "df -m __DIR__ | awk '$3 ~ /[0-9]+/ { print $4 }'",
  TOTAL = "df -m __DIR__ | awk '$3 ~ /[0-9]+/ { print $2 }'",
}

Common Patterns

Buffer vs Stream Upload

Use Buffer when:

  • File is already in memory
  • File size is small (< 10MB)
  • You need to process file content first

Use Stream when:

  • File is large (> 10MB)
  • Streaming from file system or network
  • Memory efficiency is important
// Buffer upload - simple and direct
const buffer = readFileSync('./file.pdf');
await storage.write(buffer, { filename: 'file.pdf' });

// Stream upload - memory efficient for large files
const stream = createReadStream('./large-video.mp4');
await storage.write(stream, { filename: 'video.mp4' });

Batch Upload Operations

Upload multiple files concurrently:

import { readFileSync } from 'fs';

const files = [
  readFileSync('./file1.pdf'),
  readFileSync('./file2.jpg'),
  readFileSync('./file3.doc'),
];

// 雲端適配器 (S3, GCS, R2, Azure Blob) - 不支援 options 陣列
const results = await storage.batchWrite(files);
// 檔名將自動以 hash 生成,例如: a3f2...b1c4.pdf

results.forEach(result => console.log('Uploaded:', result.key));

LocalStorage 專用: 可透過 options 陣列為每個檔案指定檔名:

import { LocalStorage } from '@rytass/storages-adapter-local';

const localStorage = new LocalStorage({ directory: './uploads', autoMkdir: true });

// LocalStorage 支援為每個檔案指定 options
const results = await localStorage.batchWrite(files, [
  { filename: 'documents/file1.pdf' },
  { filename: 'images/file2.jpg' },
  { filename: 'documents/file3.doc' },
]);

如需在雲端適配器中為每個檔案指定不同的 filename/contentType,請使用迴圈呼叫 write() 方法。

File Converters Integration

使用 converters 選項在上傳前自動處理檔案:

import { StorageS3Service } from '@rytass/storages-adapter-s3';
import { ImageResizer } from '@rytass/file-converter-adapter-image-resizer';
import { ImageTranscoder } from '@rytass/file-converter-adapter-image-transcoder';

// 建立具有自動轉換功能的 storage
const storage = new StorageS3Service({
  accessKey: process.env.AWS_ACCESS_KEY_ID!,
  secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
  bucket: 'my-bucket',
  region: 'ap-northeast-1',
  // 上傳前自動縮放並轉換為 WebP
  converters: [
    new ImageResizer({ maxWidth: 1200, maxHeight: 800, keepAspectRatio: true }),
    new ImageTranscoder({ targetFormat: 'webp', quality: 85 }),
  ],
});

// 檔案會先經過縮放和格式轉換,再上傳
const file = await storage.write(originalImageBuffer);

Custom Hash Algorithm

變更檔名雜湊演算法(預設使用 SHA256):

const storage = new StorageS3Service({
  // ...
  hashAlgorithm: 'sha1', // 或 'sha256' (預設)
});

Error Handling

All adapters throw StorageError with specific error codes:

import { StorageError, ErrorCode } from '@rytass/storages';

try {
  const file = await storage.read('non-existent-file.pdf', { format: 'buffer' });
} catch (error) {
  if (error instanceof StorageError) {
    switch (error.code) {
      case ErrorCode.FILE_NOT_FOUND:
        console.error('File not found');
        break;
      case ErrorCode.READ_FILE_ERROR:
        console.error('Failed to read file');
        break;
      default:
        console.error('Storage error:', error.message);
    }
  }
}

Error Codes:

  • WRITE_FILE_ERROR (‘101’) – Failed to upload file
  • READ_FILE_ERROR (‘102’) – Failed to download file
  • REMOVE_FILE_ERROR (‘103’) – Failed to delete file
  • UNRECOGNIZED_ERROR (‘104’) – Unknown error
  • DIRECTORY_NOT_FOUND (‘201’) – Directory not found (Local adapter)
  • FILE_NOT_FOUND (‘202’) – File does not exist

Note: Error codes are strings, not numbers.

File Existence Checking

Always check if a file exists before performing operations:

const key = 'documents/important.pdf';

// Check before reading
if (await storage.isExists(key)) {
  const file = await storage.read(key, { format: 'buffer' });
  // Process file...
} else {
  console.log('File does not exist');
}

// Check before deleting
if (await storage.isExists(key)) {
  await storage.remove(key);
  console.log('File deleted');
}

Generating Presigned URLs

Cloud adapters support generating temporary URLs for direct file access:

// S3 - NO custom expiration supported (uses default)
const s3Url = await s3Storage.url('file.pdf');

// GCS - custom expiration (timestamp in milliseconds, default: 24 hours)
const gcsUrl = await gcsStorage.url('file.pdf', Date.now() + 1000 * 60 * 30); // 30 minutes

// R2 - custom expiration (seconds in options object) with custom domain
const r2Url = await r2Storage.url('file.pdf', { expires: 1800 }); // 30 minutes in seconds

// Azure Blob - custom expiration (timestamp in milliseconds, default: 24 hours)
const azureUrl = await azureStorage.url('file.pdf', Date.now() + 1000 * 60 * 60); // 1 hour

URL Method Signatures:

Adapter Signature Expiration
S3 url(key: string) Default only
GCS url(key: string, expires?: number) Timestamp (ms), default: 24 hours
R2 url(key: string, options?: { expires?: number }) Seconds
Azure url(key: string, expires?: number) Timestamp (ms), default: 24 hours

Note: Local adapter does not support url() method as files are stored locally.

Feature Comparison

Feature S3 GCS R2 Azure Blob Local
Presigned URL ✓ ✓ ✓ ✓ ✗
Custom Domain ✗ ✗ ✓ ✗ N/A
Batch Upload ✓ ✓ ✓ ✓ ✓
Batch Upload w/ Options ✗ ✗ ✗ ✗ ✓
Buffer Support ✓ ✓ ✓ ✓ ✓
Stream Support ✓ ✓ ✓ ✓ ✓
Usage Info ✗ ✗ ✗ ✗ ✓
File Converters ✓ ✓ ✓ ✓ ✓
Hash Algorithms ✓ ✓ ✓ ✓ ✓
Auto MIME Detection ✓ ✓ ✓ ✓ ✓
Custom Filename ✓ ✓ ✓ ✓ ✓

NestJS Integration

Complete integration with NestJS dependency injection:

Basic Setup

// file-storage.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { StorageS3Service } from '@rytass/storages-adapter-s3';
import { LocalStorage } from '@rytass/storages-adapter-local';
import { Storage } from '@rytass/storages';

@Injectable()
export class FileStorageService {
  private storage: Storage;

  constructor(private configService: ConfigService) {
    const provider = this.configService.get('STORAGE_PROVIDER', 'local');

    if (provider === 's3') {
      this.storage = new StorageS3Service({
        accessKey: this.configService.get('AWS_ACCESS_KEY_ID')!,
        secretKey: this.configService.get('AWS_SECRET_ACCESS_KEY')!,
        bucket: this.configService.get('S3_BUCKET')!,
        region: this.configService.get('AWS_REGION', 'ap-northeast-1')!,
      });
    } else {
      this.storage = new LocalStorage({
        directory: this.configService.get('LOCAL_STORAGE_DIR', './uploads')!,
        autoMkdir: true,
      });
    }
  }

  async uploadFile(file: Buffer, filename: string, contentType?: string) {
    return this.storage.write(file, { filename, contentType });
  }

  async downloadFile(key: string): Promise<Buffer> {
    return this.storage.read(key, { format: 'buffer' });
  }

  async deleteFile(key: string): Promise<void> {
    return this.storage.remove(key);
  }

  async fileExists(key: string): Promise<boolean> {
    return this.storage.isExists(key);
  }

  async getFileUrl(key: string): Promise<string> {
    // Type guard to check if storage supports url()
    if ('url' in this.storage && typeof this.storage.url === 'function') {
      return this.storage.url(key);
    }
    throw new Error('Storage provider does not support presigned URLs');
  }
}

Async Configuration with ConfigService

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { FileStorageService } from './file-storage.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
  ],
  providers: [FileStorageService],
  exports: [FileStorageService],
})
export class StorageModule {}

Dynamic Provider Selection

// storage.factory.ts
import { ConfigService } from '@nestjs/config';
import { Storage } from '@rytass/storages';
import { StorageS3Service } from '@rytass/storages-adapter-s3';
import { StorageGCSService } from '@rytass/storages-adapter-gcs';
import { LocalStorage } from '@rytass/storages-adapter-local';

export const STORAGE_TOKEN = Symbol('STORAGE');

export const storageFactory = {
  provide: STORAGE_TOKEN,
  useFactory: (configService: ConfigService): Storage => {
    const provider = configService.get('STORAGE_PROVIDER', 'local');

    switch (provider) {
      case 's3':
        return new StorageS3Service({
          accessKey: configService.get('AWS_ACCESS_KEY_ID')!,
          secretKey: configService.get('AWS_SECRET_ACCESS_KEY')!,
          bucket: configService.get('S3_BUCKET')!,
          region: configService.get('AWS_REGION')!,
        });

      case 'gcs':
        return new StorageGCSService({
          bucket: configService.get('GCS_BUCKET')!,
          projectId: configService.get('GCS_PROJECT_ID')!,
          credentials: {
            client_email: configService.get('GCS_CLIENT_EMAIL')!,
            private_key: configService.get('GCS_PRIVATE_KEY')!.replace(/\\n/g, '\n'),
          },
        });

      case 'local':
      default:
        return new LocalStorage({
          directory: configService.get('LOCAL_STORAGE_DIR', './uploads')!,
          autoMkdir: true,
        });
    }
  },
  inject: [ConfigService],
};

// app.module.ts
@Module({
  providers: [storageFactory],
  exports: [STORAGE_TOKEN],
})
export class StorageModule {}

// Using in service
@Injectable()
export class UploadService {
  constructor(@Inject(STORAGE_TOKEN) private storage: Storage) {}

  async handleUpload(file: Express.Multer.File) {
    return this.storage.write(file.buffer, {
      filename: `uploads/${file.originalname}`,
      contentType: file.mimetype,
    });
  }
}

Environment Variables

# .env
STORAGE_PROVIDER=s3  # or gcs, r2, azure, local

# AWS S3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=ap-northeast-1
S3_BUCKET=my-bucket

# Google Cloud Storage
GCS_BUCKET=my-gcs-bucket
GCS_PROJECT_ID=my-project-id
GCS_CLIENT_EMAIL=service-account@project.iam.gserviceaccount.com
GCS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"

# Cloudflare R2
R2_ACCESS_KEY=your_r2_access_key
R2_SECRET_KEY=your_r2_secret_key
R2_ACCOUNT_ID=your_account_id
R2_BUCKET=my-r2-bucket
R2_CUSTOM_DOMAIN=https://cdn.example.com

# Azure Blob
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
AZURE_CONTAINER=my-container

# Local Storage
LOCAL_STORAGE_DIR=./uploads

Detailed Documentation

For complete API reference and advanced usage: