refactor: enhance storage module with S3 provider integration
- Introduced a new S3Provider class for managing file uploads, deletions, and presigned URL generation with Amazon S3. - Refactored existing storage functions to utilize the new provider, improving modularity and code organization. - Re-exported types for better accessibility and convenience. - Implemented robust error handling for storage operations, ensuring clear error messages for configuration and upload issues. - Updated the storage configuration to support S3-compatible services, enhancing flexibility.
This commit is contained in:
parent
130338b9a5
commit
9e1c648a7c
160
src/storage/README.md
Normal file
160
src/storage/README.md
Normal file
@ -0,0 +1,160 @@
|
||||
# Storage Module
|
||||
|
||||
This module provides a unified interface for storing and retrieving files using various cloud storage providers. Currently, it supports Amazon S3 and compatible services like Cloudflare R2.
|
||||
|
||||
## Features
|
||||
|
||||
- Upload files to cloud storage
|
||||
- Generate pre-signed URLs for direct browser-to-storage uploads
|
||||
- Delete files from storage
|
||||
- Client-side upload helpers for both small and large files
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { uploadFile, deleteFile, getPresignedUploadUrl } from '@/src/storage';
|
||||
|
||||
// Upload a file
|
||||
const { url, key } = await uploadFile(
|
||||
fileBuffer,
|
||||
'original-filename.jpg',
|
||||
'image/jpeg',
|
||||
'uploads/images'
|
||||
);
|
||||
|
||||
// Delete a file
|
||||
await deleteFile(key);
|
||||
|
||||
// Generate a pre-signed URL for direct upload
|
||||
const { url, key } = await getPresignedUploadUrl(
|
||||
'filename.jpg',
|
||||
'image/jpeg',
|
||||
'uploads/images'
|
||||
);
|
||||
```
|
||||
|
||||
## Client-Side Upload
|
||||
|
||||
For client-side uploads, use the `uploadFileFromBrowser` function:
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { uploadFileFromBrowser } from '@/src/storage';
|
||||
|
||||
// In your component
|
||||
async function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
try {
|
||||
// This will automatically use the most appropriate upload method
|
||||
// based on the file size
|
||||
const { url, key } = await uploadFileFromBrowser(file, 'uploads/images');
|
||||
console.log('File uploaded:', url);
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The storage module is configured using environment variables:
|
||||
|
||||
```
|
||||
# Required
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_ACCESS_KEY_ID=your-access-key
|
||||
STORAGE_SECRET_ACCESS_KEY=your-secret-key
|
||||
STORAGE_BUCKET_NAME=your-bucket-name
|
||||
|
||||
# Optional
|
||||
STORAGE_ENDPOINT=https://custom-endpoint.com
|
||||
STORAGE_PUBLIC_URL=https://cdn.example.com
|
||||
STORAGE_FORCE_PATH_STYLE=true
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Using the Storage Provider Directly
|
||||
|
||||
If you need more control, you can interact with the storage provider directly:
|
||||
|
||||
```typescript
|
||||
import { getStorageProvider } from '@/src/storage';
|
||||
|
||||
const provider = getStorageProvider();
|
||||
|
||||
// Use provider methods directly
|
||||
const result = await provider.uploadFile({
|
||||
file: fileBuffer,
|
||||
filename: 'example.pdf',
|
||||
contentType: 'application/pdf',
|
||||
folder: 'documents'
|
||||
});
|
||||
```
|
||||
|
||||
### Using a Custom Provider Implementation
|
||||
|
||||
You can create and use your own storage provider implementation:
|
||||
|
||||
```typescript
|
||||
import { StorageProvider, UploadFileParams, UploadFileResult } from '@/src/storage';
|
||||
|
||||
class CustomStorageProvider implements StorageProvider {
|
||||
// Implement the required methods
|
||||
async uploadFile(params: UploadFileParams): Promise<UploadFileResult> {
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
async getPresignedUploadUrl(params: PresignedUploadUrlParams): Promise<UploadFileResult> {
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
getProviderName(): string {
|
||||
return 'CustomProvider';
|
||||
}
|
||||
}
|
||||
|
||||
// Then use it
|
||||
const customProvider = new CustomStorageProvider();
|
||||
const result = await customProvider.uploadFile({
|
||||
file: fileBuffer,
|
||||
filename: 'example.jpg',
|
||||
contentType: 'image/jpeg'
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Main Functions
|
||||
|
||||
- `uploadFile(file, filename, contentType, folder?)`: Upload a file to storage
|
||||
- `deleteFile(key)`: Delete a file from storage
|
||||
- `getPresignedUploadUrl(filename, contentType, folder?, expiresIn?)`: Generate a pre-signed URL
|
||||
- `uploadFileFromBrowser(file, folder?)`: Upload a file from the browser
|
||||
|
||||
### Provider Interface
|
||||
|
||||
The `StorageProvider` interface defines the following methods:
|
||||
|
||||
- `uploadFile(params)`: Upload a file to storage
|
||||
- `deleteFile(key)`: Delete a file from storage
|
||||
- `getPresignedUploadUrl(params)`: Generate a pre-signed URL
|
||||
- `getProviderName()`: Get the provider name
|
||||
|
||||
### Configuration
|
||||
|
||||
The `StorageConfig` interface defines the configuration options:
|
||||
|
||||
- `region`: Storage region (e.g., 'us-east-1')
|
||||
- `endpoint?`: Custom endpoint URL for S3-compatible services
|
||||
- `accessKeyId`: Access key ID for authentication
|
||||
- `secretAccessKey`: Secret access key for authentication
|
||||
- `bucketName`: Storage bucket name
|
||||
- `publicUrl?`: Public URL for accessing files
|
||||
- `forcePathStyle?`: Whether to use path-style URLs
|
16
src/storage/config/storage-config.ts
Normal file
16
src/storage/config/storage-config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { StorageConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Default storage configuration
|
||||
*
|
||||
* This configuration is loaded from environment variables
|
||||
*/
|
||||
export const storageConfig: StorageConfig = {
|
||||
region: process.env.STORAGE_REGION || '',
|
||||
endpoint: process.env.STORAGE_ENDPOINT,
|
||||
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY || '',
|
||||
bucketName: process.env.STORAGE_BUCKET_NAME || '',
|
||||
publicUrl: process.env.STORAGE_PUBLIC_URL,
|
||||
forcePathStyle: process.env.STORAGE_FORCE_PATH_STYLE !== 'false',
|
||||
};
|
@ -1,13 +1,55 @@
|
||||
import { storageConfig } from './config/storage-config';
|
||||
import { S3Provider } from './provider/s3';
|
||||
import {
|
||||
uploadFile as s3UploadFile,
|
||||
deleteFile as s3DeleteFile,
|
||||
getPresignedUploadUrl as s3GetPresignedUploadUrl,
|
||||
StorageError,
|
||||
ConfigurationError,
|
||||
UploadError
|
||||
} from './provider/s3';
|
||||
PresignedUploadUrlParams,
|
||||
StorageConfig,
|
||||
StorageError,
|
||||
StorageProvider,
|
||||
UploadError,
|
||||
UploadFileParams,
|
||||
UploadFileResult
|
||||
} from './types';
|
||||
|
||||
export { StorageError, ConfigurationError, UploadError };
|
||||
// Re-export types for convenience
|
||||
export { ConfigurationError, StorageError, UploadError };
|
||||
export type {
|
||||
PresignedUploadUrlParams, StorageConfig, StorageProvider, UploadFileParams,
|
||||
UploadFileResult
|
||||
};
|
||||
|
||||
/**
|
||||
* Default storage configuration
|
||||
*/
|
||||
export const defaultStorageConfig: StorageConfig = storageConfig;
|
||||
|
||||
/**
|
||||
* Global storage provider instance
|
||||
*/
|
||||
let storageProvider: StorageProvider | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the storage provider
|
||||
* @returns initialized storage provider
|
||||
*/
|
||||
export const initializeStorageProvider = (): StorageProvider => {
|
||||
if (!storageProvider) {
|
||||
storageProvider = new S3Provider();
|
||||
}
|
||||
return storageProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the storage provider
|
||||
* @returns current storage provider instance
|
||||
* @throws Error if provider is not initialized
|
||||
*/
|
||||
export const getStorageProvider = (): StorageProvider => {
|
||||
if (!storageProvider) {
|
||||
return initializeStorageProvider();
|
||||
}
|
||||
return storageProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file to the configured storage provider
|
||||
@ -23,8 +65,9 @@ export const uploadFile = async (
|
||||
filename: string,
|
||||
contentType: string,
|
||||
folder?: string
|
||||
): Promise<{ url: string; key: string }> => {
|
||||
return s3UploadFile(file, filename, contentType, folder);
|
||||
): Promise<UploadFileResult> => {
|
||||
const provider = getStorageProvider();
|
||||
return provider.uploadFile({ file, filename, contentType, folder });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -34,7 +77,8 @@ export const uploadFile = async (
|
||||
* @returns Promise that resolves when the file is deleted
|
||||
*/
|
||||
export const deleteFile = async (key: string): Promise<void> => {
|
||||
return s3DeleteFile(key);
|
||||
const provider = getStorageProvider();
|
||||
return provider.deleteFile(key);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -51,8 +95,9 @@ export const getPresignedUploadUrl = async (
|
||||
contentType: string,
|
||||
folder?: string,
|
||||
expiresIn: number = 3600
|
||||
): Promise<{ url: string; key: string }> => {
|
||||
return s3GetPresignedUploadUrl(filename, contentType, folder, expiresIn);
|
||||
): Promise<UploadFileResult> => {
|
||||
const provider = getStorageProvider();
|
||||
return provider.getPresignedUploadUrl({ filename, contentType, folder, expiresIn });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -66,7 +111,7 @@ export const getPresignedUploadUrl = async (
|
||||
export const uploadFileFromBrowser = async (
|
||||
file: File,
|
||||
folder?: string
|
||||
): Promise<{ url: string; key: string }> => {
|
||||
): Promise<UploadFileResult> => {
|
||||
try {
|
||||
// For small files (< 10MB), use direct upload
|
||||
if (file.size < 10 * 1024 * 1024) {
|
||||
|
@ -1,197 +1,207 @@
|
||||
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
// Define error types for better error handling
|
||||
export class StorageError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'StorageError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationError extends StorageError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadError extends StorageError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UploadError';
|
||||
}
|
||||
}
|
||||
import { storageConfig } from '../config/storage-config';
|
||||
import {
|
||||
ConfigurationError,
|
||||
PresignedUploadUrlParams,
|
||||
StorageConfig,
|
||||
StorageError,
|
||||
StorageProvider,
|
||||
UploadError,
|
||||
UploadFileParams,
|
||||
UploadFileResult
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* S3 client configuration
|
||||
* Amazon S3 storage provider implementation
|
||||
*
|
||||
* This provider works with Amazon S3 and compatible services like Cloudflare R2
|
||||
* https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html
|
||||
* https://www.npmjs.com/package/@aws-sdk/client-s3
|
||||
* https://www.cloudflare.com/lp/pg-cloudflare-r2-vs-aws-s3/
|
||||
* https://docs.uploadthing.com/uploading-files
|
||||
* https://developers.cloudflare.com/r2/
|
||||
*/
|
||||
const getS3Client = (): S3Client => {
|
||||
const region = process.env.STORAGE_REGION;
|
||||
const endpoint = process.env.STORAGE_ENDPOINT;
|
||||
export class S3Provider implements StorageProvider {
|
||||
private config: StorageConfig;
|
||||
private s3Client: S3Client | null = null;
|
||||
|
||||
// TODO: set region to 'auto' if not set???
|
||||
if (!region) {
|
||||
throw new ConfigurationError('STORAGE_REGION environment variable is not set');
|
||||
constructor(config: StorageConfig = storageConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
const clientOptions: any = {
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId: process.env.STORAGE_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
};
|
||||
|
||||
// Add custom endpoint for S3-compatible services like Cloudflare R2
|
||||
if (endpoint) {
|
||||
clientOptions.endpoint = endpoint;
|
||||
// For services like R2 that don't use path-style URLs
|
||||
if (process.env.STORAGE_FORCE_PATH_STYLE === 'false') {
|
||||
clientOptions.forcePathStyle = false;
|
||||
} else {
|
||||
clientOptions.forcePathStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new S3Client(clientOptions);
|
||||
};
|
||||
|
||||
// Generate a unique filename with the original extension
|
||||
const generateUniqueFilename = (originalFilename: string): string => {
|
||||
const extension = originalFilename.split('.').pop() || '';
|
||||
const uuid = randomUUID();
|
||||
return `${uuid}${extension ? `.${extension}` : ''}`;
|
||||
};
|
||||
|
||||
// Upload a file to S3
|
||||
export const uploadFile = async (
|
||||
file: Buffer | Blob,
|
||||
originalFilename: string,
|
||||
contentType: string,
|
||||
folder?: string
|
||||
): Promise<{ url: string; key: string }> => {
|
||||
try {
|
||||
const s3 = getS3Client();
|
||||
const bucket = process.env.STORAGE_BUCKET_NAME;
|
||||
|
||||
if (!bucket) {
|
||||
console.error('STORAGE_BUCKET_NAME environment variable is not set');
|
||||
throw new ConfigurationError('STORAGE_BUCKET_NAME environment variable is not set');
|
||||
/**
|
||||
* Get the S3 client instance
|
||||
*/
|
||||
private getS3Client(): S3Client {
|
||||
if (this.s3Client) {
|
||||
return this.s3Client;
|
||||
}
|
||||
|
||||
const filename = generateUniqueFilename(originalFilename);
|
||||
const key = folder ? `${folder}/${filename}` : filename;
|
||||
const { region, endpoint, accessKeyId, secretAccessKey, forcePathStyle } = this.config;
|
||||
|
||||
// Convert Blob to Buffer if needed
|
||||
let fileBuffer: Buffer;
|
||||
if (file instanceof Blob) {
|
||||
fileBuffer = Buffer.from(await file.arrayBuffer());
|
||||
} else {
|
||||
fileBuffer = file;
|
||||
if (!region) {
|
||||
throw new ConfigurationError('Storage region is not configured');
|
||||
}
|
||||
|
||||
// Upload the file
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: fileBuffer,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await s3.send(command);
|
||||
|
||||
// Generate the URL
|
||||
const publicUrl = process.env.STORAGE_PUBLIC_URL;
|
||||
let url: string;
|
||||
|
||||
if (publicUrl) {
|
||||
// Use custom domain if provided
|
||||
url = `${publicUrl.replace(/\/$/, '')}/${key}`;
|
||||
console.log('uploadFile, public url', url);
|
||||
} else {
|
||||
// Generate a pre-signed URL if no public URL is provided
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
});
|
||||
url = await getSignedUrl(s3, getCommand, { expiresIn: 3600 * 24 * 7 }); // 7 days
|
||||
console.log('uploadFile, signed url', url);
|
||||
}
|
||||
|
||||
return { url, key };
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigurationError) {
|
||||
console.error('ConfigurationError', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred during file upload';
|
||||
throw new UploadError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a file from S3
|
||||
export const deleteFile = async (key: string): Promise<void> => {
|
||||
try {
|
||||
const s3 = getS3Client();
|
||||
const bucket = process.env.STORAGE_BUCKET_NAME;
|
||||
|
||||
if (!bucket) {
|
||||
throw new ConfigurationError('STORAGE_BUCKET_NAME environment variable is not set');
|
||||
}
|
||||
|
||||
const command = {
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
const clientOptions: any = {
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
};
|
||||
|
||||
await s3.send(new PutObjectCommand({
|
||||
...command,
|
||||
Body: '',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('deleteFile', error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred during file deletion';
|
||||
throw new StorageError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// Generate a pre-signed URL for direct browser uploads
|
||||
export const getPresignedUploadUrl = async (
|
||||
filename: string,
|
||||
contentType: string,
|
||||
folder?: string,
|
||||
expiresIn: number = 3600 // 1 hour default
|
||||
): Promise<{ url: string; key: string }> => {
|
||||
try {
|
||||
const s3 = getS3Client();
|
||||
const bucket = process.env.STORAGE_BUCKET_NAME;
|
||||
|
||||
if (!bucket) {
|
||||
throw new ConfigurationError('STORAGE_BUCKET_NAME environment variable is not set');
|
||||
// Add custom endpoint for S3-compatible services like Cloudflare R2
|
||||
if (endpoint) {
|
||||
clientOptions.endpoint = endpoint;
|
||||
clientOptions.forcePathStyle = forcePathStyle !== false;
|
||||
}
|
||||
|
||||
const key = folder ? `${folder}/${filename}` : filename;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(s3, command, { expiresIn });
|
||||
console.log('getPresignedUploadUrl', url);
|
||||
return { url, key };
|
||||
} catch (error) {
|
||||
console.error('getPresignedUploadUrl', error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred while generating presigned URL';
|
||||
throw new StorageError(message);
|
||||
this.s3Client = new S3Client(clientOptions);
|
||||
return this.s3Client;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique filename with the original extension
|
||||
*/
|
||||
private generateUniqueFilename(originalFilename: string): string {
|
||||
const extension = originalFilename.split('.').pop() || '';
|
||||
const uuid = randomUUID();
|
||||
return `${uuid}${extension ? `.${extension}` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to S3
|
||||
*/
|
||||
public async uploadFile(params: UploadFileParams): Promise<UploadFileResult> {
|
||||
try {
|
||||
const { file, filename, contentType, folder } = params;
|
||||
const s3 = this.getS3Client();
|
||||
const { bucketName } = this.config;
|
||||
|
||||
if (!bucketName) {
|
||||
throw new ConfigurationError('Storage bucket name is not configured');
|
||||
}
|
||||
|
||||
const uniqueFilename = this.generateUniqueFilename(filename);
|
||||
const key = folder ? `${folder}/${uniqueFilename}` : uniqueFilename;
|
||||
|
||||
// Convert Blob to Buffer if needed
|
||||
let fileBuffer: Buffer;
|
||||
if (file instanceof Blob) {
|
||||
fileBuffer = Buffer.from(await file.arrayBuffer());
|
||||
} else {
|
||||
fileBuffer = file;
|
||||
}
|
||||
|
||||
// Upload the file
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
Body: fileBuffer,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await s3.send(command);
|
||||
|
||||
// Generate the URL
|
||||
const { publicUrl } = this.config;
|
||||
let url: string;
|
||||
|
||||
if (publicUrl) {
|
||||
// Use custom domain if provided
|
||||
url = `${publicUrl.replace(/\/$/, '')}/${key}`;
|
||||
console.log('uploadFile, public url', url);
|
||||
} else {
|
||||
// Generate a pre-signed URL if no public URL is provided
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
});
|
||||
url = await getSignedUrl(s3, getCommand, { expiresIn: 3600 * 24 * 7 }); // 7 days
|
||||
console.log('uploadFile, signed url', url);
|
||||
}
|
||||
|
||||
return { url, key };
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigurationError) {
|
||||
console.error('uploadFile, configuration error', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred during file upload';
|
||||
console.error('uploadFile, error', message);
|
||||
throw new UploadError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from S3
|
||||
*/
|
||||
public async deleteFile(key: string): Promise<void> {
|
||||
try {
|
||||
const s3 = this.getS3Client();
|
||||
const { bucketName } = this.config;
|
||||
|
||||
if (!bucketName) {
|
||||
throw new ConfigurationError('Storage bucket name is not configured');
|
||||
}
|
||||
|
||||
const command = {
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
};
|
||||
|
||||
await s3.send(new PutObjectCommand({
|
||||
...command,
|
||||
Body: '',
|
||||
}));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred during file deletion';
|
||||
console.error('deleteFile, error', message);
|
||||
throw new StorageError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a pre-signed URL for direct browser uploads
|
||||
*/
|
||||
public async getPresignedUploadUrl(params: PresignedUploadUrlParams): Promise<UploadFileResult> {
|
||||
try {
|
||||
const { filename, contentType, folder, expiresIn = 3600 } = params;
|
||||
const s3 = this.getS3Client();
|
||||
const { bucketName } = this.config;
|
||||
|
||||
if (!bucketName) {
|
||||
throw new ConfigurationError('Storage bucket name is not configured');
|
||||
}
|
||||
|
||||
const uniqueFilename = this.generateUniqueFilename(filename);
|
||||
const key = folder ? `${folder}/${uniqueFilename}` : uniqueFilename;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(s3, command, { expiresIn });
|
||||
return { url, key };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred while generating presigned URL';
|
||||
console.error('getPresignedUploadUrl, error', message);
|
||||
throw new StorageError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider name
|
||||
*/
|
||||
public getProviderName(): string {
|
||||
return 'S3';
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
export { ConfigurationError, StorageError, UploadError } from '../types';
|
||||
|
||||
|
89
src/storage/types.ts
Normal file
89
src/storage/types.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Storage configuration
|
||||
*/
|
||||
export interface StorageConfig {
|
||||
region: string;
|
||||
endpoint?: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
bucketName: string;
|
||||
publicUrl?: string;
|
||||
forcePathStyle?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage provider error types
|
||||
*/
|
||||
export class StorageError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'StorageError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationError extends StorageError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConfigurationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class UploadError extends StorageError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UploadError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file parameters
|
||||
*/
|
||||
export interface UploadFileParams {
|
||||
file: Buffer | Blob;
|
||||
filename: string;
|
||||
contentType: string;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file result
|
||||
*/
|
||||
export interface UploadFileResult {
|
||||
url: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presigned upload URL parameters
|
||||
*/
|
||||
export interface PresignedUploadUrlParams {
|
||||
filename: string;
|
||||
contentType: string;
|
||||
folder?: string;
|
||||
expiresIn?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage provider interface
|
||||
*/
|
||||
export interface StorageProvider {
|
||||
/**
|
||||
* Upload a file to storage
|
||||
*/
|
||||
uploadFile(params: UploadFileParams): Promise<UploadFileResult>;
|
||||
|
||||
/**
|
||||
* Delete a file from storage
|
||||
*/
|
||||
deleteFile(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Generate a pre-signed URL for client-side uploads
|
||||
*/
|
||||
getPresignedUploadUrl(params: PresignedUploadUrlParams): Promise<UploadFileResult>;
|
||||
|
||||
/**
|
||||
* Get the provider's name
|
||||
*/
|
||||
getProviderName(): string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user