yingjieli-image-store v1.0.0
subsystem yingjieli.site
capsule://quake0day/yingjieli-image-store@1.0.0
Stores and serves artwork images for yingjieliartist.com. Uploads land
in Cloudflare R2 (binding YL_IMAGES); reads go through a small Workers
Cache layer with long-immutable cache headers and ETag support.
Owns
- POST /api/upload (multipart; admin-only)
- GET /api/img/<key> (public, cached)
- DELETE /api/img/<key> (admin-only)
- the R2 key format: <slug>_<timestamp36><rand>.{jpg|png|webp}
- the 8 MB max upload limit and the JPEG/PNG/WebP allow-list
- filename sanitization (lowercase, [a-z0-9_], max 60 chars)
Does not own
- image resizing (the client is expected to pre-resize before upload)
- mapping images to artwork records (the content-store does that via works[].file)
- who is allowed to upload/delete (delegates to yingjieli-admin-auth)
- legacy /images/* static files served directly by Cloudflare Pages
AI orientation
Images live in R2; their primary URL is /api/img/<key>. Keys are
generated server-side from the sanitized base name + a timestamp +
4 random chars, so the client cannot dictate the final key. The
Workers Cache API is used as a hot layer in front of R2 — never
cache responses behind a session or an Authorization header.
Avoid
- Trusting client-supplied keys; the server always generates them.
- Returning R2 objects through paths other than /api/img/<key>.
- Caching DELETE or admin responses.
- Allowing path traversal in keys (key.includes('/') || '..' is rejected).
Extension points
allowed-content-typesatsite/functions/api/upload.js:ALLOWED_TYPES- Set of MIME types accepted by POST /api/upload. If you add a
format, also extend pickExt() so the file gets the right extension. max-upload-bytesatsite/functions/api/upload.js:MAX_BYTES- Hard upper bound on a single upload. Increase only if R2 + Workers
request-body limits still hold.
Provides
http_api:image-upload— POST /api/upload → { ok, key, url, w, h, bytes, type }.http_api:image-serve— GET /api/img/<key> → image bytes with 1y immutable + ETag.http_api:image-delete— DELETE /api/img/<key> → admin-only purge.
Requires
library:auth-helpersfromyingjieli-admin-auth— isAuthed() guards upload and delete.env:YL_IMAGES— Cloudflare R2 bucket binding. Provided by yingjieli-cloudflare-deploy.
Dependencies
Capsules
yingjieli-admin-auth>=1.0.0 <2.0.0
Runtime
node>=18cloudflare-pages*
Invariants (must always hold)
- The server never serves an image whose R2 key was supplied verbatim by the client.
- Path-traversal patterns (`/`, `..`) are rejected with 400, never with 200.
- Public /api/img/* responses do not vary by user / cookie.
- Anonymous DELETE is impossible — auth is checked before R2.delete().
Glossary
key- the R2 object key, also the last path segment under /api/img/
pre-resize- the admin UI shrinks images client-side before POSTing
immutable- keys never change content, so Cache-Control max-age=1y immutable