-
Notifications
You must be signed in to change notification settings - Fork 0
Storage
Article30 uses RustFS as a private S3-compatible object store for uploaded documents and follow-up attachments. RustFS is not exposed to the public internet: it runs only on the internal Docker network, and the backend is the only component that talks to it.
Every document or attachment fetch goes:
browser --(session cookie + per-entity ACL)--> backend --(internal S3)--> RustFS
Earlier iterations issued AWS-style presigned URLs and let the browser fetch directly. That model required RustFS to be reachable from the public network, which conflicted with hardened deployments and made per-entity authorization difficult to enforce after the URL was minted. The current model:
- Keeps RustFS's S3 port (
:9000) private. No CDN, no TLS termination, no CORS rules to maintain. - Re-validates authorization on every fetch (the URL is not credentialed; the session cookie is).
- Works transparently with
<a href>,<img src>, and the browser's native PDF viewer (Range requests are supported).
| Method | Path | Purpose |
|---|---|---|
GET |
/api/documents/:id/download |
Stream a Document linked to a Treatment / Violation / Checklist Item. |
GET |
/api/follow-up/attachments/:id/download |
Stream a Violation- or DSR-linked attachment. |
Both endpoints return:
-
200 OK+ the file bytes for a normal request. -
206 Partial Content+Content-RangeforRangerequests (PDFs in the browser viewer issue these). -
416 Range Not Satisfiablefor an out-of-boundsRange. -
403 Forbiddenfor an unauthorised role. -
404 Not Foundfor a missing object or an ownership failure (we deliberately do not distinguish, to avoid leaking object existence via UUID enumeration).
Responses set:
-
Content-Typefrom the persistedmimeType(validated at upload), not from S3. -
Content-Disposition: inline; filename*=UTF-8''<encoded>(RFC 8187) so PNGs and PDFs preview natively while DOCX/XLSX trigger the download dialog. -
Cache-Control: private, no-storeandX-Content-Type-Options: nosniff.
| Source | Linked entity | PROCESS_OWNER allowed when... |
|---|---|---|
| Document | TREATMENT | Owns the treatment (createdBy or assignedTo). |
| Document | VIOLATION | Created/assigned the violation, OR owns a treatment linked via ViolationTreatment. |
| Document | CHECKLIST_ITEM | Always (org-wide artefact). |
| FollowUpAttachment | VIOLATION | Same as Document/VIOLATION. |
| FollowUpAttachment | DSR | Owns a treatment linked via DsrTreatmentProcessingLog. |
ADMIN, DPO, EDITOR, AUDITOR are not scoped beyond the role gate. Soft-deleted attachments (storageKey: null or deletedAt != null) return 404 for everyone.
The role gate constants live in shared/src/constants/roles.ts:
-
DOCUMENT_READ_ROLESfor/api/documents/:id/download -
FOLLOW_UP_READ_ROLESfor/api/follow-up/attachments/:id/download
-
S3_ENDPOINTshould always be the internal hostname (http://rustfs:9000in compose). It is the only S3 address the backend uses. - The dev environment runs the backend on the host, so
.env.dev.examplekeepsS3_ENDPOINT=http://localhost:9000. Thebuild/dev.compose.ymloverlay publishes port 9000 for that reason. - The RustFS console (port
9001) is independent of object access; expose or hide it per ops policy. - Backups: the
rustfsdataDocker volume holds all uploaded objects and should be snapshotted alongsidepgdata.
- Max object size: 10 MB (enforced at upload by
FileInterceptorand the service-level check). - Allowed MIME types: PDF, JPEG, PNG, DOCX, XLSX. Anything else is rejected at upload time.
- No CDN; throughput is bounded by the backend's process count and Node's stream throughput. Adequate for the current Article 30 register workload (small attachments, low concurrency); revisit if the use case grows.