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
115 lines
2.9 KiB
TypeScript
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}`;
|
|
}
|
|
}
|