localgreenchain/lib/storage/providers/s3.ts
Claude d74128d3cd
Add Agent 3: File Upload & Storage System
Implements cloud-based file storage for plant photos, documents, and certificates:

Storage Layer:
- Multi-provider support (AWS S3, Cloudflare R2, MinIO, local filesystem)
- S3-compatible provider with presigned URL generation
- Local storage provider for development with signed URL verification
- Configurable via environment variables

Image Processing:
- Automatic thumbnail generation (150x150, 300x300, 600x600, 1200x1200)
- WebP conversion for optimized file sizes
- EXIF data extraction for image metadata
- Image optimization with Sharp

API Endpoints:
- POST /api/upload/image - Upload images with automatic processing
- POST /api/upload/document - Upload documents (PDF, DOC, DOCX)
- POST /api/upload/presigned - Get presigned URLs for direct uploads
- GET/DELETE /api/upload/[fileId] - File management

UI Components:
- ImageUploader - Drag & drop image upload with preview
- PhotoGallery - Grid gallery with lightbox view
- DocumentUploader - Document upload with file type icons
- ProgressBar - Animated upload progress indicator

Database:
- FileStore service with in-memory storage (Prisma schema ready for Agent 2)
- File metadata tracking with soft delete support
- Category-based file organization
2025-11-23 03:51:31 +00:00

115 lines
2.9 KiB
TypeScript

/**
* S3 Storage Provider for LocalGreenChain
* Agent 3: File Upload & Storage System
*
* Compatible with AWS S3, Cloudflare R2, and MinIO
*/
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
HeadObjectCommand,
GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { StorageConfig, StorageProviderInterface } from '../types';
export class S3StorageProvider implements StorageProviderInterface {
private client: S3Client;
private bucket: string;
private publicUrl?: string;
constructor(config: StorageConfig) {
this.bucket = config.bucket;
this.publicUrl = config.publicUrl;
const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
region: config.region || 'us-east-1',
};
// Add endpoint for R2/MinIO
if (config.endpoint) {
clientConfig.endpoint = config.endpoint;
clientConfig.forcePathStyle = config.provider === 'minio';
}
// Add credentials if provided
if (config.accessKeyId && config.secretAccessKey) {
clientConfig.credentials = {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
};
}
this.client = new S3Client(clientConfig);
}
async upload(key: string, buffer: Buffer, contentType: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: 'public, max-age=31536000', // 1 year cache
});
await this.client.send(command);
return this.getPublicUrl(key);
}
async delete(key: string): Promise<boolean> {
try {
const command = new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
});
await this.client.send(command);
return true;
} catch (error) {
console.error('Error deleting file from S3:', error);
return false;
}
}
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.bucket,
Key: key,
});
return getSignedUrl(this.client, command, { expiresIn });
}
async getPresignedUploadUrl(key: string, contentType: string, expiresIn = 3600): Promise<string> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentType: contentType,
});
return getSignedUrl(this.client, command, { expiresIn });
}
async exists(key: string): Promise<boolean> {
try {
const command = new HeadObjectCommand({
Bucket: this.bucket,
Key: key,
});
await this.client.send(command);
return true;
} catch {
return false;
}
}
getPublicUrl(key: string): string {
if (this.publicUrl) {
return `${this.publicUrl}/${key}`;
}
return `https://${this.bucket}.s3.amazonaws.com/${key}`;
}
}