Attachments Design - JU-DEV-Bootcamps/ERAS GitHub Wiki
Attachment File Storage Proposal — Backlog
The initial implementation for attachments is currently implemented inside the Interventions feature. This feature allows to include attachment files related to the Intervention entity. Although the implementation fulfills its purpose, it has some limitations:
- Limited attachment metadata like uploaded date, original filename.
- It cannot be re-used by other entities
- Coupled with file storage provider
Current State (Baseline) — What Already Ships Today
- Storage abstraction:
IFileStorageService(src/Eras.Application/Contracts/Infrastructure/IFileStorageService.cs) exposesSaveAsync(stream, fileName, folder),ReadAsync(relativePath),DeleteAsync(relativePath)— already entity-agnostic in signature (noInterventions-specific typing). - Local provider:
LocalFileStorageService(src/Eras.Infrastructure/FileStorage/LocalFileStorageService.cs) implements the above against local disk, partitioned by folder (currentlyinterventions/{id}), storing files under a generated GUID name (original filename is not preserved on disk, only the extension). - Encryption at rest: every file is AES-encrypted before being written to disk and decrypted on read (
IFileEncryptionService/AesFileEncryptionService), with restrictive Unix file permissions applied. This property must be preserved by any future generalization. - Deduplication: uploads are SHA-256 hashed and compared against
Intervention.AttachmentHashes; identical content is skipped rather than re-saved. - Upload/download/delete flow: fully implemented but hardcoded to Interventions —
AssessmentsController→UploadInterventionAttachmentsCommand/DeleteInterventionAttachmentCommand(MediatR) →IAssessmentRepository.AddAttachmentsAsync/RemoveAttachmentAsync/GetAttachmentHashesAsync, which mutate the rawtext[]columns directly via EFCurrentValue(a workaround forinit-only properties). - Validation: extension-string matching only, against
FileStorageSettings.AllowedExtensions. No magic-byte/content sniffing.FileStorageSettings.MaxFileSizeBytesis configured but never enforced (dead config). - Metadata: no per-file metadata beyond path + hash — original file name, MIME type, size, uploader, and upload timestamp are never persisted.
- Scope: no generic
Attachmententity/table exists; no other entity in the system uploads files besidesInterventions. - In flight: an unmerged branch adds a hardcoded "max 5 attachments per intervention" rule. This should be superseded by the generic, configurable limit introduced below rather than merged as a second, competing implementation.
Proposal
The implementation proposal is based on three Stages (One EPIC for each Stage).
- Stage 1: Generalize Attachments Storage
- Stage 2: Cloud Object Storage Migration
- Stage 3 Storage Performance Optimization
mindmap
root((Attachments))
Stage 1 - Generalize Attachments Storage
1.1 - Define centralized Attachment Metadata Model
1.2 - Extend current IFileStorageService into the Generic Storage Contract
1.3 - Generalize the Local Provider's Partitioning Scheme
1.4 - Create a Generic AttachmentService and REST Endpoints
1.5 - Implement Content Validation for Uploaded Files
1.6 - Migrate Integration Attachments to New Structure
Stage 2 - Cloud Object Storage Migration
2.1 - Implement Integration with Cloud Storage Provider *OpenStack Swift*
2.2 - Keystone Authentication and Token Lifecycle Management
2.3 - Restrict Container Access and Generate Temporary Download URLs
2.4 - Implement Configurable Provider Selection
Stage 3 - Storage Performance Optimization
3.1 - Direct Client Uploads via Temporary URLs
3.2 - CDN Integration for Downloads
Everything in Epic 2 (cloud storage) and Epic 3 (performance) is not implemented at the moment.