storage-adapters
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 forConvertableFile)StorageFile- Object withreadonly key: stringpropertyWriteFileOptions-{ filename?: string; contentType?: string }FilenameHashAlgorithm-'sha1' | 'sha256'FileKey- Type alias forstringReadBufferFileOptions-{ 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 fileREAD_FILE_ERROR('102') - Failed to download fileREMOVE_FILE_ERROR('103') - Failed to delete fileUNRECOGNIZED_ERROR('104') - Unknown errorDIRECTORY_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:
More from rytass/utils
wms-module
|
24logistics-development
|
13logistics-adapters
|
12invoice-adapters
Taiwan e-invoice integration (台灣電子發票整合). Use when working with ECPay (綠界), EZPay (藍新), BankPro (金財通), or Amego (光貿) invoice services. Covers issuing invoices (開立發票), voiding (作廢), allowances (折讓), and querying invoice data.
8payment-adapters
Taiwan payment integration (台灣支付整合). Use when working with ECPay (綠界), NewebPay (藍新), HwaNan Bank (華南銀行), CTBC (中信), iCash Pay, or Happy Card payment services. Covers credit card (信用卡), virtual account (虛擬帳號), ATM, CVS payment (超商代碼/條碼), card binding (卡片綁定), installments (分期付款), recurring payments (訂閱付款), and NestJS integration.
7member-module
|
7