Persist kagane covers in Postgres (#49)

Closes #43

Persist kagane cover bytes in a dedicated Postgres covers table keyed by image ID. The web handler reads storage before the browser, writes validated fetches through, and no longer keeps an in-process cover cache. Added migration, store persistence tests including reopen, handler coverage for stored/miss/rejected paths, and corrected repository guidance.

Verification:
- go test ./...
- CGO_ENABLED=0 go build ./...

Reviewed-on: #49
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #49.
This commit is contained in:
2026-08-09 06:52:02 +07:00
committed by sulthan
parent cd3a7e3d01
commit bfae84c5c3
7 changed files with 180 additions and 77 deletions
+31
View File
@@ -529,6 +529,37 @@ func scanSeries(scan func(...any) error) (Series, error) {
// Close releases the underlying database handle.
func (s *Store) Close() error { return s.db.Close() }
// GetKaganeCover returns one persisted cover. Missing covers are reported with
// ok=false rather than as an error so the web handler can fetch them once.
func (s *Store) GetKaganeCover(imageID string) ([]byte, string, bool, error) {
var (
body []byte
contentType string
)
err := s.db.QueryRow(
`SELECT body, content_type FROM covers WHERE image_id = $1`, imageID,
).Scan(&body, &contentType)
if errors.Is(err, sql.ErrNoRows) {
return nil, "", false, nil
}
if err != nil {
return nil, "", false, fmt.Errorf("get kagane cover %q: %w", imageID, err)
}
return body, contentType, true, nil
}
// PutKaganeCover persists one fetched cover. Image ids are immutable, so a
// later fetch cannot replace the bytes already made durable.
func (s *Store) PutKaganeCover(imageID string, body []byte, contentType string) error {
if _, err := s.db.Exec(`
INSERT INTO covers (image_id, body, content_type)
VALUES ($1, $2, $3)
ON CONFLICT (image_id) DO NOTHING`, imageID, body, contentType); err != nil {
return fmt.Errorf("put kagane cover %q: %w", imageID, err)
}
return nil
}
// List returns every bookmark of one reader, newest activity first.
// Series-owned fields are joined in, so each Bookmark reads back whole and
// flat (ADR-0004).