refactor(handlers): consume port interfaces, wire media+upload assembly (Task 25+26)

- H now holds only small port interfaces (UserStore/LibraryStore/BookStore/
  ProgressStore/BookmarkStore/RateLimiter/Scanner/Media/UploadSessions);
  NewRouter is the composition root distributing *store.Store and *redispkg.R
- ports.Media gains EnsurePage; ChaptersOf returns ports.Chapter (media's
  local duplicate dropped); *media.M now provably satisfies ports.Media;
  ports.UploadSessions gains LibraryID for root validation before Complete
- content.go: duplicated page-index cache + cover self-heal + page extract
  logic removed in favor of media service — same redis keys, same contract;
  path traversal check stays in handler (403 semantics preserved)
- getLibrary/getLibRow merged into getLib(c, id); idParam helper dedupes
  :id parsing; isUnique replaced by ports.IsUniqueViolation (Task 28 partial)
- main.go assembles media + upload and passes upload.U as scanner Sweeper

Full gate green: gofmt, vet, go test -p 1 (real PG+Redis, 0 skip)
This commit is contained in:
2026-09-14 22:56:15 +08:00
parent b9b7022212
commit 7baadedeb4
15 changed files with 186 additions and 245 deletions
+8 -10
View File
@@ -13,6 +13,7 @@ import (
"booklib/internal/bookfile"
"booklib/internal/config"
"booklib/internal/ports"
"booklib/internal/redispkg"
)
@@ -22,6 +23,9 @@ type M struct {
rdb *redispkg.R
}
// compile-time proof that *M satisfies the consumer-side interface.
var _ ports.Media = (*M)(nil)
func New(cfg *config.Config, rdb *redispkg.R) *M {
return &M{cfg: cfg, rdb: rdb}
}
@@ -79,7 +83,7 @@ func (m *M) PageIndex(ctx context.Context, bookID int64, size, modTS int64, root
}
// ChaptersOf derives chapters from a page index by grouping pages by parent directory.
func (m *M) ChaptersOf(idx []string) []Chapter {
func (m *M) ChaptersOf(idx []string) []ports.Chapter {
type grp struct {
dir string
start int
@@ -101,9 +105,9 @@ func (m *M) ChaptersOf(idx []string) []Chapter {
return nil
}
titles := make(map[string]int)
out := make([]Chapter, len(grps))
out := make([]ports.Chapter, len(grps))
for i, g := range grps {
out[i] = Chapter{Title: path.Base(g.dir), Start: g.start}
out[i] = ports.Chapter{Title: path.Base(g.dir), Start: g.start}
titles[out[i].Title]++
}
for i, g := range grps {
@@ -114,14 +118,8 @@ func (m *M) ChaptersOf(idx []string) []Chapter {
return out
}
// Chapter represents a CBZ chapter.
type Chapter struct {
Title string `json:"title"`
Start int `json:"start"`
}
// EnsurePage extracts a single page to the cache. Returns the cache file path.
func (m *M) EnsurePage(bookID int64, size, modTS int64, root, rel string, n int, idx []string) (string, error) {
func (m *M) EnsurePage(_ context.Context, bookID int64, size, modTS int64, root, rel string, n int, idx []string) (string, error) {
if n >= len(idx) {
return "", fmt.Errorf("page %d out of range", n)
}
+8
View File
@@ -78,6 +78,9 @@ type Scanner interface {
type UploadSessions interface {
Init(ctx context.Context, libID int64, name string, size, chunkSize int64) (string, error)
// LibraryID returns the target library recorded in the session, so the
// handler can resolve+validate the library root before Complete.
LibraryID(ctx context.Context, uploadID string) (int64, error)
Status(ctx context.Context, uploadID string) ([]int64, error)
PutPart(ctx context.Context, uploadID string, index int64, body io.Reader, maxSize int64) error
Complete(ctx context.Context, uploadID string, root string) (string, error)
@@ -86,7 +89,12 @@ type UploadSessions interface {
}
type Media interface {
// EnsureCover extracts+caches the cover if not already cached. Returns nil
// for formats without cover support.
EnsureCover(ctx context.Context, bookID int64, format string, size, modTS int64, root, rel string) error
// EnsurePage extracts page n (from a pre-fetched idx) to the cache and
// returns the cache file path.
EnsurePage(ctx context.Context, bookID int64, size, modTS int64, root, rel string, n int, idx []string) (string, error)
ChaptersOf(idx []string) []Chapter
PageIndex(ctx context.Context, bookID int64, size, modTS int64, root, rel string) ([]string, error)
CacheBuster(size, modTS int64) string