/** * 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 { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); } async upload(key: string, buffer: Buffer, contentType: string): Promise { 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 { 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 { // 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 { // 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 { 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; } }