Files
XingfenD 7baadedeb4 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)
2026-09-14 22:56:15 +08:00

108 lines
4.4 KiB
Go

// Package ports defines consumer-side interfaces for the backend services.
// Handlers depend on these interfaces, not on concrete implementations.
// Main.go wires concrete types (*store.Store, *redispkg.R, etc.) that satisfy them.
//
// Value types (User, Book, etc.) live in internal/store and are referenced here.
package ports
import (
"context"
"io"
"time"
"booklib/internal/store"
)
// ---------- Store interfaces ----------
type UserStore interface {
CountUsers(ctx context.Context) (int, error)
CreateUser(ctx context.Context, username, hash, role string) (int64, error)
GetUserByName(ctx context.Context, username string) (store.User, error)
GetUserByID(ctx context.Context, id int64) (store.User, error)
ListUsers(ctx context.Context) ([]store.User, error)
DeleteUser(ctx context.Context, id int64) error
}
type LibraryStore interface {
CreateLibrary(ctx context.Context, name, root string) (int64, error)
ListLibraries(ctx context.Context) ([]store.Library, error)
GetLibrary(ctx context.Context, id int64) (store.Library, error)
}
type BookStore interface {
InsertBook(ctx context.Context, libID int64, path, title, format string, size, modTS int64, pageCount int) (int64, error)
GetBook(ctx context.Context, id int64) (store.Book, error)
ListBookMeta(ctx context.Context, libID int64) (map[string]store.BookMeta, error)
UpdateBookFile(ctx context.Context, id, size, modTS int64, pageCount int) error
DeleteBookByPath(ctx context.Context, libID int64, path string) error
DeleteBook(ctx context.Context, id int64) error
SetBookState(ctx context.Context, id int64, state, msg string) error
ListBooks(ctx context.Context, libID int64, q, prefix string, userID int64) ([]store.BookView, error)
BookHashes(ctx context.Context) (map[int64][2]int64, error)
}
type ProgressStore interface {
UpsertProgress(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64) error
GetProgress(ctx context.Context, userID, libID int64, bookPath string) (store.Progress, error)
ListProgress(ctx context.Context, userID int64) ([]store.Progress, error)
}
type BookmarkStore interface {
InsertBookmark(ctx context.Context, userID, libID int64, bookPath string, locator []byte, percent float64, note string) (int64, error)
ListBookmarks(ctx context.Context, userID, libID int64, bookPath string) ([]store.Bookmark, error)
UpdateBookmarkNote(ctx context.Context, userID, id int64, note string) (bool, error)
DeleteBookmark(ctx context.Context, userID, id int64) (bool, error)
}
// ---------- Redis interfaces ----------
type PageCache interface {
Get(ctx context.Context, key string) (string, bool)
Set(ctx context.Context, key, val string, ttl time.Duration)
}
type RateLimiter interface {
IncrWindow(ctx context.Context, key string, ttl time.Duration) int
}
type ScanLocker interface {
ScanLock(ctx context.Context, key string, ttl time.Duration) (func(), bool)
}
// ---------- Service interfaces ----------
type Scanner interface {
ScanLibraryByID(ctx context.Context, id int64)
}
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)
Sweep(ctx context.Context) error
UniquePath(root, name string) (string, error)
}
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
}
// Chapter represents a CBZ chapter derived from the archive's folder structure.
type Chapter struct {
Title string `json:"title"`
Start int `json:"start"`
}