File storage service and image upload API #27

Closed
opened 2026-07-04 14:52:10 +02:00 by fable-5 · 1 comment
Collaborator

Context

Images pasted into pages are stored on the uploads volume with quota accounting (ADR 0011).

Scope

Implement FileStorage (filesystem binding, layout uploads/<pondId>/<fileId>), attachments model, POST /ponds/:id/files (multipart; MIME sniffing; images only for now — the general allowlist arrives in M6; size limit via QuotaService; storage quota consumption), GET /media/:fileId (permission-checked streaming, correct Content-Type, nosniff, cache headers), DELETE /files/:id (quota release).

Acceptance criteria

  • upload → serve roundtrip preserves bytes and content type
  • non-image uploads are rejected in M2 scope; oversize and quota-exceeded return distinct localized errors
  • magic-byte check: a renamed .html-as-.png is rejected
  • deleting releases quota; pond_usage.storage_bytes_used matches actual volume content in a test
  • files of a soft-deleted page stay until purge (assert not deleted eagerly)

Technical notes

  • ADR 0011, security.md §Uploads.
  • SVG: reject in M2 (sanitization decision point comes with M6 allowlist work).

Dependencies

Depends on #22, #23.

Size: ~1.5 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Images pasted into pages are stored on the uploads volume with quota accounting (ADR 0011). ## Scope Implement `FileStorage` (filesystem binding, layout `uploads/<pondId>/<fileId>`), `attachments` model, `POST /ponds/:id/files` (multipart; MIME sniffing; images only for now — the general allowlist arrives in M6; size limit via `QuotaService`; storage quota consumption), `GET /media/:fileId` (permission-checked streaming, correct Content-Type, `nosniff`, cache headers), `DELETE /files/:id` (quota release). ## Acceptance criteria - [ ] upload → serve roundtrip preserves bytes and content type - [ ] non-image uploads are rejected in M2 scope; oversize and quota-exceeded return distinct localized errors - [ ] magic-byte check: a renamed .html-as-.png is rejected - [ ] deleting releases quota; `pond_usage.storage_bytes_used` matches actual volume content in a test - [ ] files of a soft-deleted page stay until purge (assert not deleted eagerly) ## Technical notes - ADR 0011, security.md §Uploads. - SVG: reject in M2 (sanitization decision point comes with M6 allowlist work). ## Dependencies Depends on #22, #23. **Size**: ~1.5 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M2 — Ponds & pages, single-user editing milestone 2026-07-04 14:52:10 +02:00
fable-5 added the
backend
label 2026-07-04 14:52:10 +02:00
Collaborator

Implemented in 0fae699 and closed via Closes #27. Pipeline verification for that commit:

  • CI / Lint, typecheck, test — success
  • CI / Auth e2e pack — success
  • CD / Build and push images — success
  • CD / Deploy to Test — success
  • CD / Smoke tests against Test — success
  • CD / Promote to Int — success

All acceptance criteria covered by apps/api/src/files/files.e2e.db.test.ts: upload/serve roundtrip (bytes + sniffed content type), non-image rejection, magic-byte check (renamed .html-as-.png), oversize vs. storage-quota-exceeded as distinct errors, delete releases quota and removes the on-disk bytes, and a soft-deleted page's attachment stays reachable (no eager purge).

Implemented in 0fae699 and closed via `Closes #27`. Pipeline verification for that commit: - CI / Lint, typecheck, test — success - CI / Auth e2e pack — success - CD / Build and push images — success - CD / Deploy to Test — success - CD / Smoke tests against Test — success - CD / Promote to Int — success All acceptance criteria covered by `apps/api/src/files/files.e2e.db.test.ts`: upload/serve roundtrip (bytes + sniffed content type), non-image rejection, magic-byte check (renamed `.html`-as-`.png`), oversize vs. storage-quota-exceeded as distinct errors, delete releases quota and removes the on-disk bytes, and a soft-deleted page's attachment stays reachable (no eager purge).
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#27
No description provided.