mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-19 12:06:40 +08:00
136 lines
5.3 KiB
Go
136 lines
5.3 KiB
Go
// Command bat-api is the resource bootstrap and distribution HTTP service for BlueArchiveToolkit.
|
|
//
|
|
// Responsibility boundary:
|
|
// - bat (Rust): official resource auto-discover, pull, verify, publish, daemon RPC
|
|
// - bat-api (Go): startup resource bootstrap, server-info rewrite,
|
|
// read-only distribution of published resources (CDN-shaped paths), release
|
|
// inspection APIs, and normal process configuration (config.toml /
|
|
// process environment / flags for listen port, RPC socket, reserved
|
|
// database/redis settings)
|
|
//
|
|
// bat-api discovers and periodically refreshes the current release through the
|
|
// bat.sock JSON-RPC contract (daemon.status first, then daemon.doctor, then
|
|
// catalog/resource methods). The production resource root comes from RPC; the
|
|
// resource-root override is for local fixtures or emergency diagnostics.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"bat-api/internal/api"
|
|
"bat-api/internal/backendrpc"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
|
|
log.SetPrefix("bat-api ")
|
|
|
|
cfg := api.DefaultConfig()
|
|
if err := api.LoadConfigFromCurrentExe(&cfg); err != nil {
|
|
log.Fatalf("load config.toml: %v", err)
|
|
}
|
|
api.ApplyEnv(&cfg)
|
|
|
|
var err error
|
|
cfg, err = parseFlags(os.Args[1:], cfg)
|
|
if err != nil {
|
|
if err == flag.ErrHelp {
|
|
return
|
|
}
|
|
log.Fatalf("parse flags: %v", err)
|
|
}
|
|
// If socket still empty after flags, derive from state-dir.
|
|
if cfg.SocketPath == "" {
|
|
cfg.SocketPath = filepath.Join(cfg.StateDir, "bat.sock")
|
|
}
|
|
if err := cfg.Normalize(); err != nil {
|
|
log.Fatalf("config: %v", err)
|
|
}
|
|
|
|
var backend api.Backend
|
|
client := backendrpc.New(cfg.SocketPath)
|
|
client.Timeout = cfg.RPCTimeout
|
|
backend = api.RPCClient{Client: client}
|
|
|
|
server := api.NewServer(cfg, backend, log.Default())
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
refreshCtx, refreshCancel := context.WithTimeout(ctx, cfg.RPCTimeout+5*time.Second)
|
|
if err := server.Refresh(refreshCtx); err != nil {
|
|
log.Printf("initial discover failed: %v (serving with empty/partial index)", err)
|
|
}
|
|
refreshCancel()
|
|
server.StartRefreshLoop(ctx)
|
|
|
|
if err := server.ListenAndServe(ctx); err != nil && err != context.Canceled {
|
|
log.Fatalf("serve: %v", err)
|
|
}
|
|
}
|
|
|
|
func parseFlags(args []string, cfg api.Config) (api.Config, error) {
|
|
flags := flag.NewFlagSet("bat-api", flag.ContinueOnError)
|
|
flags.SetOutput(os.Stderr)
|
|
|
|
listen := flags.String("listen", cfg.Listen, "HTTP listen address")
|
|
publicBase := flags.String("public-base-url", cfg.PublicBaseURL, "public base URL for Addressables rewrite")
|
|
stateDir := flags.String("state-dir", cfg.StateDir, "bat daemon state dir (derives default socket)")
|
|
socket := flags.String("socket", cfg.SocketPath, "path to bat.sock JSON-RPC socket (primary discovery)")
|
|
resourceRoot := flags.String("resource-root", cfg.ResourceRoot, "override published release root (tests/emergency)")
|
|
serverInfo := flags.String("server-info-file", cfg.ServerInfoFile, "optional server-info JSON path")
|
|
requireIndexed := flags.Bool("require-indexed", cfg.RequireIndexed, "only serve files present in the release index")
|
|
verifySize := flags.Bool("verify-size", cfg.VerifySize, "reject CDN files whose size differs from the index")
|
|
rpcTimeout := flags.Duration("rpc-timeout", cfg.RPCTimeout, "daemon RPC timeout")
|
|
refreshInterval := flags.Duration("refresh-interval", cfg.RefreshInterval, "periodic release discovery interval (0 disables)")
|
|
authToken := flags.String("auth-token", cfg.AuthToken, "HTTP management token")
|
|
authQueryParam := flags.String("auth-query-param", cfg.AuthQueryParam, "query parameter accepted for token auth fallback")
|
|
authExemptPaths := flags.String("auth-exempt-paths", strings.Join(cfg.AuthExemptPaths, ","), "comma-separated auth-exempt exact paths or slash-prefixes")
|
|
trustProxyHeaders := flags.Bool("trust-proxy-headers", cfg.TrustProxyHeaders, "trust X-Forwarded-For and X-Real-IP from reverse proxy")
|
|
accessLog := flags.Bool("access-log", cfg.AccessLog, "enable per-request access logs without query strings")
|
|
rateLimitRPS := flags.Float64("rate-limit-rps", cfg.RateLimitRPS, "per-client request rate limit; 0 disables")
|
|
rateLimitBurst := flags.Int("rate-limit-burst", cfg.RateLimitBurst, "per-client rate limit burst")
|
|
maxResourceLimit := flags.Int("max-resource-limit", cfg.MaxResourcePageLimit, "maximum /v1/resources page size")
|
|
if err := flags.Parse(args); err != nil {
|
|
return cfg, err
|
|
}
|
|
|
|
cfg.Listen = *listen
|
|
cfg.PublicBaseURL = *publicBase
|
|
cfg.StateDir = *stateDir
|
|
cfg.SocketPath = *socket
|
|
cfg.ResourceRoot = *resourceRoot
|
|
cfg.ServerInfoFile = *serverInfo
|
|
cfg.RequireIndexed = *requireIndexed
|
|
cfg.VerifySize = *verifySize
|
|
cfg.RPCTimeout = *rpcTimeout
|
|
cfg.RefreshInterval = *refreshInterval
|
|
cfg.AuthToken = *authToken
|
|
cfg.AuthQueryParam = *authQueryParam
|
|
cfg.AuthExemptPaths = splitFlagCSV(*authExemptPaths)
|
|
cfg.TrustProxyHeaders = *trustProxyHeaders
|
|
cfg.AccessLog = *accessLog
|
|
cfg.RateLimitRPS = *rateLimitRPS
|
|
cfg.RateLimitBurst = *rateLimitBurst
|
|
cfg.MaxResourcePageLimit = *maxResourceLimit
|
|
return cfg, nil
|
|
}
|
|
|
|
func splitFlagCSV(raw string) []string {
|
|
var out []string
|
|
for _, part := range strings.Split(raw, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|