Move cover bytes to content-addressed filesystem storage (#65)

Refs #56

## Summary

Moves Kagane cover bytes out of Postgres bytea storage into an immutable, content-addressed filesystem store. Reader-visible behavior remains unchanged: the existing session-gated route serves stored bytes, missing bytes use the existing browser fetch path, and no browser still returns a missing cover.

## Changes

- Added migration 0008, which drops the legacy `covers` table and recreates it with only `address`, `path`, and `content_type`. Existing byte rows are intentionally dropped.
- Added SHA-256 source-URL addressing with two-level sharding (`ab/cd/<sha256>`). Writes use a temp file plus atomic link; reads validate the stored relative path before opening it.
- Made `COVER_DIR` required in runtime config and Compose. Compose passes it as a Docker build argument and volume target, so custom durable paths keep image ownership, runtime config, and the named `cover-data` volume aligned.
- Updated every `store.Open` caller and documented configuration, deployment, backup, and troubleshooting behavior.
- Added filesystem, restart, migration-drop, no-browser, and content-addressing coverage.

## Verification

- `go test ./...`
- `CGO_ENABLED=0 go build ./...`
- `docker build --build-arg COVER_DIR=/data/covers -t manga-bookmark-cover-check-custom ./backend`
- `docker compose config --format json` confirms custom `COVER_DIR` is the volume target
- `git diff --check origin/main`
- LSP diagnostics clean for touched Go files

Parents #47 and #55 remain open as required by #56.

Reviewed-on: #65
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #65.
This commit is contained in:
2026-08-10 00:11:20 +07:00
committed by sulthan
parent 30c57bd39c
commit e8d1cba6c5
15 changed files with 264 additions and 42 deletions
+10 -1
View File
@@ -11,7 +11,10 @@
services:
bookmark-api:
build: ./backend
build:
context: ./backend
args:
COVER_DIR: ${COVER_DIR:?set COVER_DIR in .env}
image: bookmarkmanager-backend:latest
container_name: bookmark-api
restart: unless-stopped
@@ -26,6 +29,9 @@ services:
# The bookmarks database. Host is the compose service name; the password
# comes from .env so it is never committed.
DATABASE_URL: ${DATABASE_URL:-postgres://bookmarks:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/bookmarks?sslmode=disable}
# Required path inside the API. The build seeds ownership at this path
# and the named volume below mounts there.
COVER_DIR: ${COVER_DIR:?set COVER_DIR in .env}
PORT: "8080"
# Log timestamps only. Go's `log` stamps lines in local time, and this
# service has no other use for a zone: bookmark timestamps are unix ms
@@ -80,6 +86,8 @@ services:
# no rebuild, no restart. `git pull` restores the committed version, which
# is why a redeploy always ships the repo's script.
- ./userscript:/userscript:ro
# Content-addressed cover bytes survive API restarts and redeploys.
- cover-data:${COVER_DIR:?set COVER_DIR in .env}
# Bound to loopback only: the proxy (or curl during smoke test) reaches it,
# the public internet does not.
ports:
@@ -112,6 +120,7 @@ services:
volumes:
postgres-data:
cover-data:
# The pre-Postgres SQLite volume (bookmarks-data) is deliberately no longer
# declared here: undeclared means `docker compose down -v` cannot take it
# with the rest, so the old database survives the cutover until someone