/** * File Store for LocalGreenChain * Agent 3: File Upload & Storage System * * In-memory file metadata store (to be replaced with Prisma when Agent 2 completes) * * Prisma Schema (for Agent 2 to integrate): * * model File { * id String @id @default(cuid()) * filename String // Storage path/key * originalName String // Original uploaded filename * mimeType String * size Int // File size in bytes * category String // plant-photo, certificate, document, report, avatar * url String // Public URL * thumbnailUrl String? // Thumbnail URL (for images) * urls Json? // All size variants URLs * width Int? // Image width * height Int? // Image height * exifData Json? // EXIF metadata * uploadedById String? // User who uploaded * plantId String? // Associated plant * farmId String? // Associated farm * createdAt DateTime @default(now()) * updatedAt DateTime @updatedAt * deletedAt DateTime? // Soft delete * * uploadedBy User? @relation(fields: [uploadedById], references: [id]) * plant Plant? @relation(fields: [plantId], references: [id]) * farm VerticalFarm? @relation(fields: [farmId], references: [id]) * * @@index([category]) * @@index([plantId]) * @@index([farmId]) * @@index([uploadedById]) * } */ import { FileMetadata, FileCategory, ImageSize } from './types'; /** * In-memory file store * This is a temporary implementation until Prisma is set up by Agent 2 */ class FileStore { private files: Map = new Map(); /** * Save file metadata */ async save(metadata: FileMetadata): Promise { this.files.set(metadata.id, { ...metadata, updatedAt: new Date(), }); return metadata; } /** * Get file by ID */ async getById(id: string): Promise { const file = this.files.get(id); if (!file || file.deletedAt) { return null; } return file; } /** * Get file by filename/key */ async getByFilename(filename: string): Promise { for (const file of this.files.values()) { if (file.filename === filename && !file.deletedAt) { return file; } } return null; } /** * Get files by plant ID */ async getByPlantId(plantId: string): Promise { const results: FileMetadata[] = []; for (const file of this.files.values()) { if (file.plantId === plantId && !file.deletedAt) { results.push(file); } } return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } /** * Get files by farm ID */ async getByFarmId(farmId: string): Promise { const results: FileMetadata[] = []; for (const file of this.files.values()) { if (file.farmId === farmId && !file.deletedAt) { results.push(file); } } return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } /** * Get files by user ID */ async getByUserId(userId: string): Promise { const results: FileMetadata[] = []; for (const file of this.files.values()) { if (file.uploadedBy === userId && !file.deletedAt) { results.push(file); } } return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } /** * Get files by category */ async getByCategory(category: FileCategory): Promise { const results: FileMetadata[] = []; for (const file of this.files.values()) { if (file.category === category && !file.deletedAt) { results.push(file); } } return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } /** * Update file metadata */ async update(id: string, updates: Partial): Promise { const existing = this.files.get(id); if (!existing) { return null; } const updated: FileMetadata = { ...existing, ...updates, id, // Ensure ID cannot be changed updatedAt: new Date(), }; this.files.set(id, updated); return updated; } /** * Soft delete a file */ async softDelete(id: string): Promise { const existing = this.files.get(id); if (!existing) { return false; } this.files.set(id, { ...existing, deletedAt: new Date(), updatedAt: new Date(), }); return true; } /** * Hard delete a file */ async hardDelete(id: string): Promise { return this.files.delete(id); } /** * List all files with pagination */ async list(options: { limit?: number; offset?: number; includeDeleted?: boolean; } = {}): Promise<{ files: FileMetadata[]; total: number }> { const { limit = 50, offset = 0, includeDeleted = false } = options; let results = Array.from(this.files.values()); if (!includeDeleted) { results = results.filter((f) => !f.deletedAt); } // Sort by creation date descending results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); const total = results.length; const files = results.slice(offset, offset + limit); return { files, total }; } /** * Get storage statistics */ async getStats(): Promise<{ totalFiles: number; totalSize: number; byCategory: Record; }> { const stats: { totalFiles: number; totalSize: number; byCategory: Record; } = { totalFiles: 0, totalSize: 0, byCategory: { 'plant-photo': { count: 0, size: 0 }, 'certificate': { count: 0, size: 0 }, 'document': { count: 0, size: 0 }, 'report': { count: 0, size: 0 }, 'avatar': { count: 0, size: 0 }, }, }; for (const file of this.files.values()) { if (!file.deletedAt) { stats.totalFiles++; stats.totalSize += file.size; stats.byCategory[file.category].count++; stats.byCategory[file.category].size += file.size; } } return stats; } /** * Clear all files (for testing) */ clear(): void { this.files.clear(); } } // Singleton instance let fileStoreInstance: FileStore | null = null; export function getFileStore(): FileStore { if (!fileStoreInstance) { fileStoreInstance = new FileStore(); } return fileStoreInstance; } export { FileStore };