feat: implement storage provider with local and S3 adapters
This commit is contained in:
138
src/lib/storage/storage.adapter.ts
Normal file
138
src/lib/storage/storage.adapter.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
StorageProvider,
|
||||
StorageResult,
|
||||
} from '@/lib/storage/storage.interface';
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Local file system storage adapter
|
||||
* Saves files to the public/uploads directory and returns local paths
|
||||
*/
|
||||
export class LocalStorageAdapter implements StorageProvider {
|
||||
private readonly uploadsDir: string;
|
||||
|
||||
constructor(uploadsDir: string = 'public/uploads') {
|
||||
this.uploadsDir = uploadsDir;
|
||||
}
|
||||
|
||||
async put(
|
||||
key: string,
|
||||
file: Buffer | Blob,
|
||||
_contentType: string
|
||||
): Promise<StorageResult> {
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(this.uploadsDir, { recursive: true });
|
||||
|
||||
// Convert Blob to Buffer if needed
|
||||
const buffer =
|
||||
file instanceof Blob ? Buffer.from(await file.arrayBuffer()) : file;
|
||||
|
||||
// Write file to disk
|
||||
const filePath = path.join(this.uploadsDir, key);
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
// Return local path as public URL
|
||||
const publicUrl = `/${this.uploadsDir}/${key}`;
|
||||
|
||||
return {
|
||||
type: 'local',
|
||||
provider: null,
|
||||
key,
|
||||
publicUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const filePath = path.join(this.uploadsDir, key);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
// File might not exist, silently ignore
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'code' in error &&
|
||||
error.code !== 'ENOENT'
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS S3 storage adapter
|
||||
* Uploads files to S3 and returns public URLs
|
||||
*/
|
||||
export class S3StorageAdapter implements StorageProvider {
|
||||
private readonly s3Client: S3Client;
|
||||
private readonly bucketName: string;
|
||||
private readonly region: string;
|
||||
|
||||
constructor(
|
||||
bucketName: string,
|
||||
region: string = 'us-east-1',
|
||||
s3Client?: S3Client
|
||||
) {
|
||||
this.bucketName = bucketName;
|
||||
this.region = region;
|
||||
this.s3Client =
|
||||
s3Client ||
|
||||
new S3Client({
|
||||
region: this.region,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async put(
|
||||
key: string,
|
||||
file: Buffer | Blob,
|
||||
contentType: string
|
||||
): Promise<StorageResult> {
|
||||
// Convert Blob to Buffer if needed
|
||||
const buffer =
|
||||
file instanceof Blob ? Buffer.from(await file.arrayBuffer()) : file;
|
||||
|
||||
// Upload to S3
|
||||
await this.s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: contentType,
|
||||
})
|
||||
);
|
||||
|
||||
// Generate public URL
|
||||
const publicUrl = `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${key}`;
|
||||
|
||||
return {
|
||||
type: 's3',
|
||||
provider: 'aws-s3',
|
||||
key,
|
||||
publicUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
try {
|
||||
await this.s3Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: key,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
// Log error but don't throw to avoid cascading failures
|
||||
console.error(`Failed to delete S3 object: ${key}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/lib/storage/storage.factory.ts
Normal file
28
src/lib/storage/storage.factory.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
LocalStorageAdapter,
|
||||
S3StorageAdapter,
|
||||
} from '@/lib/storage/storage.adapter';
|
||||
import { StorageProvider } from '@/lib/storage/storage.interface';
|
||||
|
||||
/**
|
||||
* Factory function to create the appropriate storage provider based on environment
|
||||
*/
|
||||
export function createStorageProvider(): StorageProvider {
|
||||
const storageType = process.env.STORAGE_TYPE || 'local';
|
||||
|
||||
if (storageType === 's3') {
|
||||
const bucketName = process.env.S3_BUCKET_NAME;
|
||||
const region = process.env.S3_REGION || 'us-east-1';
|
||||
|
||||
if (!bucketName) {
|
||||
throw new Error(
|
||||
'S3_BUCKET_NAME environment variable is required when STORAGE_TYPE=s3'
|
||||
);
|
||||
}
|
||||
|
||||
return new S3StorageAdapter(bucketName, region);
|
||||
}
|
||||
|
||||
// Default to local storage
|
||||
return new LocalStorageAdapter();
|
||||
}
|
||||
34
src/lib/storage/storage.interface.ts
Normal file
34
src/lib/storage/storage.interface.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Result returned from storage operations
|
||||
*/
|
||||
export interface StorageResult {
|
||||
type: 'local' | 's3';
|
||||
provider: string | null;
|
||||
key: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage provider interface for abstracting different storage implementations
|
||||
*/
|
||||
export interface StorageProvider {
|
||||
/**
|
||||
* Uploads a file to storage
|
||||
* @param key - The unique key/path for the file
|
||||
* @param file - The file content as Buffer or Blob
|
||||
* @param contentType - MIME type of the file
|
||||
* @returns Promise<StorageResult> - Result containing storage metadata and public URL
|
||||
*/
|
||||
put(
|
||||
key: string,
|
||||
file: Buffer | Blob,
|
||||
contentType: string
|
||||
): Promise<StorageResult>;
|
||||
|
||||
/**
|
||||
* Deletes a file from storage
|
||||
* @param key - The unique key/path of the file to delete
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
9
src/lib/storage/storage.ts
Normal file
9
src/lib/storage/storage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createStorageProvider } from '@/lib/storage/storage.factory';
|
||||
|
||||
/**
|
||||
* Singleton instance of the configured storage provider
|
||||
*/
|
||||
export const storage: ReturnType<typeof createStorageProvider> =
|
||||
createStorageProvider();
|
||||
|
||||
export type { StorageResult } from '@/lib/storage/storage.interface';
|
||||
Reference in New Issue
Block a user