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) exposes SaveAsync(stream, fileName, folder), ReadAsync(relativePath), DeleteAsync(relativePath) — already entity-agnostic in signature (no Interventions-specific typing).
  • Local provider: LocalFileStorageService (src/Eras.Infrastructure/FileStorage/LocalFileStorageService.cs) implements the above against local disk, partitioned by folder (currently interventions/{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 — AssessmentsControllerUploadInterventionAttachmentsCommand / DeleteInterventionAttachmentCommand (MediatR) → IAssessmentRepository.AddAttachmentsAsync / RemoveAttachmentAsync / GetAttachmentHashesAsync, which mutate the raw text[] columns directly via EF CurrentValue (a workaround for init-only properties).
  • Validation: extension-string matching only, against FileStorageSettings.AllowedExtensions. No magic-byte/content sniffing. FileStorageSettings.MaxFileSizeBytes is 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 Attachment entity/table exists; no other entity in the system uploads files besides Interventions.
  • 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).

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.