| name | storage-adapters |
| description | File storage adapters (檔案儲存適配器). Use when working with AWS S3, Google Cloud Storage (GCS), Cloudflare R2, Azure Blob, or local file storage (本地儲存). Covers file upload (檔案上傳), download (下載), presigned URLs (簽署 URL), batch operations (批量操作), and NestJS integration. |
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:
interface StorageInterface {
write(file: InputFile, options?: WriteFileOptions): Promise<StorageFile>;
batchWrite(files: InputFile[]): Promise<StorageFile[]>;
read(key: string): Promise<Readable>;
read(key: string, options: ReadBufferFileOptions): Promise<Buffer>;
read(key: string, options: ReadStreamFileOptions): Promise<Readable>;
remove(key: string): Promise<void>;
}
interface ReadBufferFileOptions {
format: 'buffer';
}
interface ReadStreamFileOptions {
format: 'stream';
}
注意: 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;
}
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]>;
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>;
}
Installation
npm install @rytass/storages
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';
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',
});
const buffer = readFileSync('./document.pdf');
const file1 = await storage.write(buffer, {
filename: 'documents/report.pdf',
contentType: 'application/pdf',
});
console.log('Uploaded:', file1.key);
const stream = createReadStream('./image.jpg');
const file2 = await storage.write(stream);
console.log('Uploaded:', file2.key);
const downloadedBuffer = await storage.read(file1.key, { format: 'buffer' });
const downloadedStream = await storage.read(file1.key, { format: 'stream' });
const url = await storage.url(file1.key);
console.log('Presigned URL:', url);
await storage.remove(file1.key);
const exists = await storage.isExists(file1.key);
console.log('Exists:', exists);
Google Cloud Storage
import { StorageGCSService } from '@rytass/storages-adapter-gcs';
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'),
},
});
const file = await storage.write(buffer, {
filename: 'uploads/file.pdf',
});
const url = await storage.url(file.key, Date.now() + 1000 * 60 * 60);
Cloudflare R2
import { StorageR2Service } from '@rytass/storages-adapter-r2';
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',
});
const file = await storage.write(buffer);
const url = await storage.url(file.key, { expires: 3600 });
console.log('Custom domain URL:', url);
Azure Blob Storage
import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob';
const storage = new StorageAzureBlobService({
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING!,
container: 'my-container',
});
const file = await storage.write(buffer, {
filename: 'files/document.pdf',
});
const url = await storage.url(file.key, Date.now() + 1000 * 60 * 60 * 24);
Local File System
import { LocalStorage, StorageLocalUsageInfo } from '@rytass/storages-adapter-local';
const storage = new LocalStorage({
directory: './uploads',
autoMkdir: true,
});
const file = await storage.write(buffer, {
filename: 'documents/file.pdf',
});
const downloadedBuffer = await storage.read(file.key, { format: 'buffer' });
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';
interface StorageLocalOptions extends StorageOptions {
directory: string;
autoMkdir?: boolean;
}
interface StorageLocalUsageInfo {
used: number;
free: number;
total: number;
}
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
const buffer = readFileSync('./file.pdf');
await storage.write(buffer, { filename: 'file.pdf' });
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'),
];
const results = await storage.batchWrite(files);
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 });
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';
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',
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',
});
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';
if (await storage.isExists(key)) {
const file = await storage.read(key, { format: 'buffer' });
} else {
console.log('File does not exist');
}
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:
const s3Url = await s3Storage.url('file.pdf');
const gcsUrl = await gcsStorage.url('file.pdf', Date.now() + 1000 * 60 * 30);
const r2Url = await r2Storage.url('file.pdf', { expires: 1800 });
const azureUrl = await azureStorage.url('file.pdf', Date.now() + 1000 * 60 * 60);
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
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> {
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
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
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],
};
@Module({
providers: [storageFactory],
exports: [STORAGE_TOKEN],
})
export class StorageModule {}
@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
STORAGE_PROVIDER=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=ap-northeast-1
S3_BUCKET=my-bucket
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"
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_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
AZURE_CONTAINER=my-container
LOCAL_STORAGE_DIR=./uploads
Detailed Documentation
For complete API reference and advanced usage: