Persist kagane covers so they survive restarts and a sleeping browser #43

Closed
opened 2026-08-09 03:35:25 +07:00 by sulthan · 2 comments
Owner

Part of #38

What to build

A kagane cover, once fetched, is served from the backend's own storage forever after — including
across restarts and while the headless browser is asleep or unreachable.

Today covers live in an in-process map. Every backend restart empties it, and every miss goes back
through the browser. After this ticket the first request for a given cover reaches the browser at
most once ever; a deploy no longer blanks the library.

Implementation decisions

  • A new covers table keyed by the kagane image id, holding the image bytes, the content type, and
    a fetch timestamp. Keyed by image id because that is the identifier the request actually carries:
    serving becomes a primary-key lookup rather than a scan for the series row whose stored cover URL
    happens to contain that UUID.
  • Deliberately a separate table rather than columns on series. It is then structurally impossible
    for a future query or a SELECT * to drag image bytes into the poller's hot path, instead of
    relying on the convention that a column list stays explicit. Orphan rows are theoretical —
    nothing in the store deletes a series row, and a test already pins that.
  • The store gains a read and a write for cover bytes. The blob is not added to the series column
    list used by the due query.
  • The web handler reads from the store first. On a miss it falls back to the browser exactly as
    today and writes through to the store.
  • The in-process cover cache and its accompanying ponytail: note are deleted. The store is the
    cache now, and unlike the map it survives a restart.
  • The cover fetcher interface the handler depends on is unchanged in shape, so the existing handler
    tests and their counting fake keep working.
  • Unchanged and non-negotiable: the UUID validation at the request boundary (the stored value is
    client-supplied, so the id is validated before it reaches either the database or the browser), the
    content-type allowlist, the session gate, and the long immutable cache headers.
  • The URL shape the templates render is unchanged.

Acceptance criteria

  • A cover already in the store is served without the browser being consulted at all.
  • A miss fetches through the browser, persists, and a second request for the same id is served
    from storage with no further browser call.
  • Cover bytes survive a process restart: the same served result with a browser fetcher that
    would fail if called.
  • Every existing rejection path yields the same result as before: a non-UUID id, a UUID with a
    trailing path segment, a challenged fetch, a content type outside the allowlist, a
    path-traversal attempt, an unauthenticated request.
  • A response that was rejected by the allowlist is not persisted.
  • The in-process cover cache no longer exists in the codebase.
  • go test ./... passes.

Blocked by

None — can start immediately.

Part of #38 ## What to build A kagane cover, once fetched, is served from the backend's own storage forever after — including across restarts and while the headless browser is asleep or unreachable. Today covers live in an in-process map. Every backend restart empties it, and every miss goes back through the browser. After this ticket the first request for a given cover reaches the browser at most once ever; a deploy no longer blanks the library. ## Implementation decisions - A new `covers` table keyed by the kagane image id, holding the image bytes, the content type, and a fetch timestamp. Keyed by image id because that is the identifier the request actually carries: serving becomes a primary-key lookup rather than a scan for the series row whose stored cover URL happens to contain that UUID. - Deliberately a separate table rather than columns on `series`. It is then structurally impossible for a future query or a `SELECT *` to drag image bytes into the poller's hot path, instead of relying on the convention that a column list stays explicit. Orphan rows are theoretical — nothing in the store deletes a series row, and a test already pins that. - The store gains a read and a write for cover bytes. The blob is **not** added to the series column list used by the due query. - The web handler reads from the store first. On a miss it falls back to the browser exactly as today and writes through to the store. - The in-process cover cache and its accompanying `ponytail:` note are deleted. The store is the cache now, and unlike the map it survives a restart. - The cover fetcher interface the handler depends on is unchanged in shape, so the existing handler tests and their counting fake keep working. - Unchanged and non-negotiable: the UUID validation at the request boundary (the stored value is client-supplied, so the id is validated before it reaches either the database or the browser), the content-type allowlist, the session gate, and the long immutable cache headers. - The URL shape the templates render is unchanged. ## Acceptance criteria - [x] A cover already in the store is served without the browser being consulted at all. - [x] A miss fetches through the browser, persists, and a second request for the same id is served from storage with no further browser call. - [x] Cover bytes survive a process restart: the same served result with a browser fetcher that would fail if called. - [x] Every existing rejection path yields the same result as before: a non-UUID id, a UUID with a trailing path segment, a challenged fetch, a content type outside the allowlist, a path-traversal attempt, an unauthenticated request. - [x] A response that was rejected by the allowlist is not persisted. - [x] The in-process cover cache no longer exists in the codebase. - [x] `go test ./...` passes. ## Blocked by None — can start immediately.
sulthan added the ready-for-agent label 2026-08-09 03:35:25 +07:00
Author
Owner

Implemented on branch issue-43. Added Postgres-backed covers storage with migration 0007, store-first serving, write-through persistence, and restart coverage; removed the in-process cover cache. Preserved UUID/session/content-type/traversal rejection paths and immutable cache headers. Verification: go test ./... and CGO_ENABLED=0 go build ./... pass. Pull request follows.

Implemented on branch issue-43. Added Postgres-backed covers storage with migration 0007, store-first serving, write-through persistence, and restart coverage; removed the in-process cover cache. Preserved UUID/session/content-type/traversal rejection paths and immutable cache headers. Verification: go test ./... and CGO_ENABLED=0 go build ./... pass. Pull request follows.
Author
Owner

PR #49 opened: #49. It targets main and includes Closes #43; issue will close when merged.

PR #49 opened: https://gitea.violetcrown.my.id/sulthan/mangaBookmark/pulls/49. It targets main and includes Closes #43; issue will close when merged.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#43