Files
zpan/server
saltboandClaude Opus 4.7 4f47a81e5b feat(image-upload): prefer R2 binding on Cloudflare, fall back to S3
On Cloudflare Workers deployments with PUBLIC_IMAGES (R2 binding) +
PUBLIC_IMAGES_URL (env) configured, writes go through the R2 binding
directly — zero auth overhead, zero egress cost, no AWS SDK in the hot
path. Reads are served straight from R2's managed public domain; no
Worker round-trip per image.

Non-CF deployments (Node/Docker/Lambda/Vercel/etc.) keep the existing
behavior: select the DB-configured mode='public' storage row and use
S3Service. The only requirement is that users still add a public
storage via Admin → Storages.

`uploadPublicImage(platform, prefix, id, file)` and
`deletePublicImageVariants(platform, prefix, id)` now take a Platform
rather than a bare Database. They internally pick the backend:

  getBackend:
    if getBinding('PUBLIC_IMAGES') && getEnv('PUBLIC_IMAGES_URL')
      → R2 backend
    else if selectStorage(db, 'public') succeeds
      → S3 backend
    else
      → none (returns 503)

The R2Bucket type is declared locally (minimal structural shape) so we
avoid pulling @cloudflare/workers-types into non-CF builds.

12 new unit tests in image-upload.test.ts cover both backends —
mime/size validation, URL construction (including trailing-slash
normalization + jpeg→jpg extension), delete-all-variants, and the
fallback precedence matrix (binding alone / URL alone / neither →
expected result).

Callers updated:
- server/routes/me.ts — PUT/DELETE /avatar
- server/routes/teams.ts — PUT/DELETE /:teamId/logo

Existing integration tests (54 cases across me + teams) continue to
pass via the S3 fallback path — they use mockPlatform without a binding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:56:35 -04:00
..