localgreenchain/lib/storage/providers/local.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

131 lines
3.6 KiB
TypeScript

/**
* Local Storage Provider for LocalGreenChain
* Agent 3: File Upload & Storage System
*
* Filesystem-based storage for development and testing
*/
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
import { StorageConfig, StorageProviderInterface } from '../types';
export class LocalStorageProvider implements StorageProviderInterface {
private basePath: string;
private publicUrl: string;
constructor(config: StorageConfig) {
this.basePath = config.localPath || './uploads';
this.publicUrl = config.publicUrl || 'http://localhost:3001';
}
private getFilePath(key: string): string {
return path.join(this.basePath, key);
}
private async ensureDirectory(filePath: string): Promise<void> {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
}
async upload(key: string, buffer: Buffer, contentType: string): Promise<string> {
const filePath = this.getFilePath(key);
await this.ensureDirectory(filePath);
await fs.writeFile(filePath, buffer);
// Write metadata file
const metadataPath = `${filePath}.meta.json`;
await fs.writeFile(
metadataPath,
JSON.stringify({
contentType,
size: buffer.length,
uploadedAt: new Date().toISOString(),
})
);
return this.getPublicUrl(key);
}
async delete(key: string): Promise<boolean> {
try {
const filePath = this.getFilePath(key);
await fs.unlink(filePath);
// Also delete metadata file if exists
try {
await fs.unlink(`${filePath}.meta.json`);
} catch {
// Metadata file may not exist
}
return true;
} catch (error) {
console.error('Error deleting local file:', error);
return false;
}
}
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
// For local storage, we generate a signed URL using HMAC
const expires = Date.now() + expiresIn * 1000;
const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret';
const signature = crypto
.createHmac('sha256', secret)
.update(`${key}:${expires}`)
.digest('hex')
.substring(0, 16);
return `${this.publicUrl}/api/upload/${encodeURIComponent(key)}?expires=${expires}&sig=${signature}`;
}
async getPresignedUploadUrl(key: string, contentType: string, expiresIn = 3600): Promise<string> {
// For local storage, return an API endpoint for upload
const expires = Date.now() + expiresIn * 1000;
const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret';
const signature = crypto
.createHmac('sha256', secret)
.update(`upload:${key}:${contentType}:${expires}`)
.digest('hex')
.substring(0, 16);
return `${this.publicUrl}/api/upload/presigned?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(contentType)}&expires=${expires}&sig=${signature}`;
}
async exists(key: string): Promise<boolean> {
try {
await fs.access(this.getFilePath(key));
return true;
} catch {
return false;
}
}
getPublicUrl(key: string): string {
return `${this.publicUrl}/uploads/${key}`;
}
/**
* Verify a signed URL signature
*/
static verifySignature(
key: string,
expires: number,
signature: string,
prefix = ''
): boolean {
if (Date.now() > expires) {
return false;
}
const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret';
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${prefix}${key}:${expires}`)
.digest('hex')
.substring(0, 16);
return signature === expectedSignature;
}
}