Split backend into internal packages by responsibility
All Go files lived flat in backend/ as one package main. Move store,
latest-chapter polling, sessions, HTTP middleware, the JSON API, the
userscript handler, and the web UI (with its templates/static assets)
into backend/internal/{store,latest,session,httpmw,api,userscript,web},
each with an exported API. main.go becomes the composition root wiring
them into newRouter; root-level tests cover the assembled router while
package-local tests cover unit behavior. Update Dockerfile/.dockerignore
for the new internal/ tree and CLAUDE.md to describe the layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+35
-28
@@ -11,6 +11,13 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"mangabm/backend/internal/api"
|
||||
"mangabm/backend/internal/httpmw"
|
||||
"mangabm/backend/internal/latest"
|
||||
"mangabm/backend/internal/store"
|
||||
"mangabm/backend/internal/userscript"
|
||||
"mangabm/backend/internal/web"
|
||||
)
|
||||
|
||||
// Config holds all runtime settings, sourced from environment variables.
|
||||
@@ -149,22 +156,22 @@ func loadConfig() Config {
|
||||
// newRouter wires routes and middleware. CORS is the outermost layer so
|
||||
// preflight OPTIONS short-circuits before auth; /bookmarks* is auth-protected,
|
||||
// /healthz is public.
|
||||
func newRouter(store *Store, cfg Config) http.Handler {
|
||||
func newRouter(s *store.Store, cfg Config) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", healthz)
|
||||
mux.HandleFunc("GET /healthz", api.Healthz)
|
||||
|
||||
// Outside withAuth (the updater sends no Authorization header) and outside
|
||||
// the WEB_PASSWORD gate (the script must be installable either way). The
|
||||
// path segment carries the token instead.
|
||||
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscriptHandler(cfg.Token, cfg.UserscriptPath))
|
||||
// Outside httpmw.Auth (the updater sends no Authorization header) and
|
||||
// outside the WEB_PASSWORD gate (the script must be installable either
|
||||
// way). The path segment carries the token instead.
|
||||
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscript.Handler(cfg.Token, cfg.UserscriptPath))
|
||||
|
||||
h := &bookmarkHandler{store: store}
|
||||
h := &api.Handler{Store: s}
|
||||
protected := http.NewServeMux()
|
||||
protected.HandleFunc("GET /bookmarks", h.list)
|
||||
protected.HandleFunc("PUT /bookmarks/{key}", h.put)
|
||||
protected.HandleFunc("DELETE /bookmarks/{key}", h.delete)
|
||||
protected.HandleFunc("GET /bookmarks", h.List)
|
||||
protected.HandleFunc("PUT /bookmarks/{key}", h.Put)
|
||||
protected.HandleFunc("DELETE /bookmarks/{key}", h.Delete)
|
||||
|
||||
auth := withAuth(cfg.Token, protected)
|
||||
auth := httpmw.Auth(cfg.Token, protected)
|
||||
mux.Handle("/bookmarks", auth)
|
||||
mux.Handle("/bookmarks/", auth)
|
||||
|
||||
@@ -172,14 +179,14 @@ func newRouter(store *Store, cfg Config) http.Handler {
|
||||
// deployment that forgets WEB_PASSWORD exposes nothing rather than
|
||||
// exposing an unprotected list.
|
||||
if cfg.WebPassword != "" {
|
||||
web, err := newWebHandler(store, cfg)
|
||||
wh, err := web.New(s, cfg.Token, cfg.WebPassword)
|
||||
if err != nil {
|
||||
log.Fatalf("web handler: %v", err)
|
||||
}
|
||||
web.register(mux)
|
||||
wh.Register(mux)
|
||||
}
|
||||
|
||||
return withCORS(cfg.AllowedOrigins, withGzip(guardEmptyUserscriptToken(mux)))
|
||||
return httpmw.CORS(cfg.AllowedOrigins, httpmw.Gzip(guardEmptyUserscriptToken(mux)))
|
||||
}
|
||||
|
||||
// guardEmptyUserscriptToken heads off ServeMux's own path-cleaning redirect:
|
||||
@@ -203,22 +210,22 @@ func main() {
|
||||
log.Fatal("API_TOKEN is required")
|
||||
}
|
||||
|
||||
store, err := OpenStore(cfg.DBPath)
|
||||
s, err := store.Open(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("open store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
defer s.Close()
|
||||
|
||||
// The poller is off the request path entirely: if it cannot start, the
|
||||
// service still serves bookmarks and the userscript still captures latest
|
||||
// chapters on its own.
|
||||
pollCtx, stopPoll := context.WithCancel(context.Background())
|
||||
defer stopPoll()
|
||||
startLatestPoller(pollCtx, store, cfg.LatestPoll)
|
||||
startLatestPoller(pollCtx, s, cfg.LatestPoll)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: newRouter(store, cfg),
|
||||
Handler: newRouter(s, cfg),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
@@ -248,24 +255,24 @@ func main() {
|
||||
// HTTP client cannot be built. Any problem here is logged and skipped: this
|
||||
// feature going missing degrades the service to userscript-only latest-chapter
|
||||
// tracking, which is exactly how it behaved before.
|
||||
func startLatestPoller(ctx context.Context, store *Store, cfg LatestPoll) {
|
||||
func startLatestPoller(ctx context.Context, s *store.Store, cfg LatestPoll) {
|
||||
if !cfg.Enabled {
|
||||
log.Println("latest-chapter poller: disabled by config")
|
||||
return
|
||||
}
|
||||
f, err := newTLSFetcher()
|
||||
f, err := latest.NewTLSFetcher()
|
||||
if err != nil {
|
||||
log.Printf("latest-chapter poller: disabled, cannot build client: %v", err)
|
||||
return
|
||||
}
|
||||
p := &latestPoller{
|
||||
store: store,
|
||||
fetch: f,
|
||||
now: time.Now,
|
||||
cooldown: cfg.Cooldown,
|
||||
interval: cfg.Interval,
|
||||
stagger: cfg.Stagger,
|
||||
batch: cfg.Batch,
|
||||
p := &latest.Poller{
|
||||
Store: s,
|
||||
Fetch: f,
|
||||
Now: time.Now,
|
||||
Cooldown: cfg.Cooldown,
|
||||
Interval: cfg.Interval,
|
||||
Stagger: cfg.Stagger,
|
||||
Batch: cfg.Batch,
|
||||
}
|
||||
go p.Run(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user