- 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)
108 lines
4.4 KiB
Go
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"`
|
|
}
|