feat: implement storage provider with local and S3 adapters

This commit is contained in:
2026-04-10 00:59:35 -03:00
parent fb8c07d32e
commit 98515550ca
8 changed files with 2260 additions and 0 deletions

View 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);
}
}
}