S3: JPEG support for avatar upload + serve #19

Open
opened 2026-05-12 23:45:12 +02:00 by arne · 1 comment
Owner

Parent

posta/server#15 — Operator-uploaded avatar served from the identity's URL

What to build

Extend the imagesniff module and the GET route to accept JPEG alongside
PNG. Unblocks iOS clients, which convert HEIC (camera roll default) to
JPEG before upload and would otherwise fail the PNG-only sniff in S1.

End-to-end behaviour after this slice:

  • PUT /api/v1/identity/avatar with Content-Type: image/jpeg and
    valid JPEG bytes succeeds. The avatar is stored as
    avatar-<hash>.jpg under the per-identity directory; the actor doc's
    avatar URL ends in .jpg.
  • GET /avatar-<hash>.jpg on the identity's host returns 200,
    Content-Type: image/jpeg, same Cache-Control: max-age=31536000, immutable and ETag behaviour as the PNG path.
  • Uploading PNG bytes declared as image/jpeg (or vice versa) still
    fails with 400 content-type-mismatch.
  • Uploading HEIC bytes (with any declared Content-Type) fails with
    400 not-an-image — HEIC is intentionally out of scope and the
    rejection makes that visible to clients.
  • A PNG→JPEG re-upload deletes the prior avatar-*.png and leaves
    only the new avatar-<hash>.jpg (existing storage behaviour from S1
    is unchanged; just verify it works across extensions).

Acceptance criteria

  • Imagesniff module recognises JPEG: accepts the standard SOI
    marker \xff\xd8\xff followed by the byte \xe0 (JFIF) /
    \xe1 (EXIF) / \xdb (raw quantization-table — common from
    some encoders).
  • Imagesniff returns ("jpg", "image/jpeg", nil) for valid JPEG.
  • Test fixtures extend the S1 table with: valid JPEG (\xff\xd8\xff\xe0),
    valid JPEG (\xff\xd8\xff\xe1), HEIC ftyp brand (rejection:
    not-an-image), PNG bytes declared as image/jpeg (rejection:
    content-type-mismatch).
  • PUT handler accepts Content-Type: image/jpeg as a valid declared
    type. (Anything else still rejects with unsupported-media-type.)
  • GET route pattern extended to match .jpg as well as .png
    (single pattern for either extension is fine).
  • HTTP-level test covers: PUT JPEG → identity.avatar column reflects
    a .jpg URL → GET on that URL returns Content-Type: image/jpeg
    with the bytes intact.
  • Test for the PNG→JPEG transition: PUT PNG, then PUT JPEG; the
    per-identity directory contains exactly one file
    (avatar-<newhash>.jpg), and GET on the old .png URL returns
    404.
  • go build ./... and go test ./... pass.

Blocked by

posta/server#16 (S1: PNG avatar upload + public GET). Imagesniff,
avatar storage, PUT handler, and GET route all land in S1; this slice
extends them.

## Parent posta/server#15 — Operator-uploaded avatar served from the identity's URL ## What to build Extend the imagesniff module and the GET route to accept JPEG alongside PNG. Unblocks iOS clients, which convert HEIC (camera roll default) to JPEG before upload and would otherwise fail the PNG-only sniff in S1. End-to-end behaviour after this slice: - `PUT /api/v1/identity/avatar` with `Content-Type: image/jpeg` and valid JPEG bytes succeeds. The avatar is stored as `avatar-<hash>.jpg` under the per-identity directory; the actor doc's `avatar` URL ends in `.jpg`. - `GET /avatar-<hash>.jpg` on the identity's host returns `200`, `Content-Type: image/jpeg`, same `Cache-Control: max-age=31536000, immutable` and `ETag` behaviour as the PNG path. - Uploading PNG bytes declared as `image/jpeg` (or vice versa) still fails with `400 content-type-mismatch`. - Uploading HEIC bytes (with any declared Content-Type) fails with `400 not-an-image` — HEIC is intentionally out of scope and the rejection makes that visible to clients. - A PNG→JPEG re-upload deletes the prior `avatar-*.png` and leaves only the new `avatar-<hash>.jpg` (existing storage behaviour from S1 is unchanged; just verify it works across extensions). ## Acceptance criteria - [ ] Imagesniff module recognises JPEG: accepts the standard SOI marker `\xff\xd8\xff` followed by the byte `\xe0` (JFIF) / `\xe1` (EXIF) / `\xdb` (raw quantization-table — common from some encoders). - [ ] Imagesniff returns `("jpg", "image/jpeg", nil)` for valid JPEG. - [ ] Test fixtures extend the S1 table with: valid JPEG (`\xff\xd8\xff\xe0`), valid JPEG (`\xff\xd8\xff\xe1`), HEIC `ftyp` brand (rejection: `not-an-image`), PNG bytes declared as `image/jpeg` (rejection: `content-type-mismatch`). - [ ] PUT handler accepts `Content-Type: image/jpeg` as a valid declared type. (Anything else still rejects with `unsupported-media-type`.) - [ ] GET route pattern extended to match `.jpg` as well as `.png` (single pattern for either extension is fine). - [ ] HTTP-level test covers: PUT JPEG → identity.avatar column reflects a `.jpg` URL → GET on that URL returns `Content-Type: image/jpeg` with the bytes intact. - [ ] Test for the PNG→JPEG transition: PUT PNG, then PUT JPEG; the per-identity directory contains exactly one file (`avatar-<newhash>.jpg`), and `GET` on the old `.png` URL returns `404`. - [ ] `go build ./...` and `go test ./...` pass. ## Blocked by posta/server#16 (S1: PNG avatar upload + public GET). Imagesniff, avatar storage, PUT handler, and GET route all land in S1; this slice extends them.
Author
Owner

This was generated by AI during triage.

Extends imagesniff and the GET route to accept JPEG alongside PNG. iOS clients convert HEIC (camera-roll default) to JPEG before upload, so this slice unblocks iOS launch in tandem with #16.

Acceptance criteria above already function as an agent brief.

Category: enhancement
State: ready-for-agent

> *This was generated by AI during triage.* Extends imagesniff and the GET route to accept JPEG alongside PNG. iOS clients convert HEIC (camera-roll default) to JPEG before upload, so this slice unblocks iOS launch in tandem with #16. Acceptance criteria above already function as an agent brief. **Category:** enhancement **State:** ready-for-agent
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
posta/server#19
No description provided.