Implement file validation logic in AttachmentUploadService
epic-document-attachments-services-task-003 — Implement the validation layer inside AttachmentUploadService: enforce 10 MB maximum file size by reading the byte length before upload, and validate MIME type against an allowlist of PDF (application/pdf), JPEG (image/jpeg), and PNG (image/png). Return typed Either<AttachmentUploadError, ValidatedFile> so callers receive structured failures without exceptions.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use the `mime` Dart package or a custom magic-byte map to detect MIME from file content. Define an `AttachmentUploadError` sealed class (Dart 3 sealed) with variants: `fileTooLarge`, `unsupportedMimeType`, `emptyFile`, and `unknown`. The `ValidatedFile` value object should be immutable and contain: `originalFilename` (sanitized), `byteLength`, `mimeType`, and `bytes` (Uint8List). Use the `fpdart` or `dartz` package for `Either` if already in the dependency graph; otherwise implement a lightweight local Either.
Do not introduce a new functional programming dependency just for this task if one is not already present — a simple sealed Result class is acceptable. Keep the validation function pure (no side effects) to make it trivially testable.
Testing Requirements
Unit tests (flutter_test) must cover: (1) file exactly at 10 MB boundary is accepted, (2) file one byte over is rejected, (3) each allowlisted MIME type is accepted via magic bytes, (4) each rejected MIME type (GIF, WEBP, HEIC, MP4) returns the correct error variant, (5) empty file returns emptyFile error, (6) file with PDF extension but JPEG magic bytes is classified as image/jpeg (not application/pdf), (7) validateFile returns Right
The storage upload succeeds but the subsequent metadata insert fails. The rollback delete call to Supabase Storage could itself fail (network error, transient timeout), leaving an orphaned object in the bucket with no database record pointing to it — a cost and compliance risk that also breaks delete-on-cascade logic.
Mitigation & Contingency
Mitigation: Wrap the rollback delete in a retry loop (3 attempts, exponential back-off). Log orphaned-object incidents to a dedicated structured log stream for periodic audit. Consider a scheduled Supabase Edge Function that reconciles storage objects against database records and flags orphans.
Contingency: If orphaned objects accumulate, run the reconciliation edge function manually to identify and purge them. Add a monitoring alert for metadata insert failures after successful uploads so the issue is caught within minutes.
If the signed URL TTL is set too short, users browsing the attachment preview modal on slow connections will receive expired URLs before the content loads, causing a broken experience. If set too long, a URL shared outside the app (e.g., pasted into a chat) remains valid beyond the intended access window.
Mitigation & Contingency
Mitigation: Default TTL to 60 minutes, configurable via a named constant. The in-memory cache TTL should be set to TTL minus 5 minutes to ensure cached URLs are refreshed before they expire. Document the trade-off in code comments.
Contingency: If users report broken previews, shorten the cache TTL hotfix. If a URL leak is reported, rotate the Supabase storage signing secret to invalidate all outstanding signed URLs immediately.
The multi-attachment user story requires parallel uploads with individual progress indicators. Managing concurrent BLoC events for 3–5 simultaneous uploads risks state collisions, progress indicator mixups, or partial rollbacks that are difficult to reason about.
Mitigation & Contingency
Mitigation: Design the BLoC to maintain a per-attachment upload state map keyed by a client-generated UUID. Each upload runs as an isolated Future with its own result emitted as a typed event. Write integration tests for 3-concurrent-upload scenarios.
Contingency: If state collisions occur in production, fall back to sequential upload processing (one at a time) gated behind a feature flag until the concurrent model is stabilised.