mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:14:55 +08:00
feat(api): 补齐资源分发服务入口
新增 bat-api 资源 bootstrap/分发 HTTP 服务、RPC release 发现、CDN path 分发、launcher 资源引导兼容、控制面中间件、OpenAPI 和 systemd 模板。 同步 Go 边界文档,明确 Rust bat 是资源生产者和同步运维入口,Go bat-api 是只读 bootstrap/分发服务,试验 Go CLI 产物为 bin/bat-go。 验证:未运行新命令;本轮已按要求停止重复构建/测试。
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# bat-api configuration example (copy to .env next to the binary or export)
|
||||
# Priority: CLI flags > process environment > .env > built-in defaults.
|
||||
#
|
||||
# Boundary:
|
||||
# - Rust bat: resource auto-discover / pull / verify / publish / daemon RPC
|
||||
# - bat-api: resource bootstrap + read-only distribution (official CDN-shaped paths)
|
||||
# + management APIs
|
||||
|
||||
BAT_API_LISTEN=:18080
|
||||
BAT_API_PUBLIC_BASE_URL=http://127.0.0.1:18080
|
||||
|
||||
# Primary discovery: bat daemon JSON-RPC socket file
|
||||
BAT_API_STATE_DIR=/tmp/bat-pid
|
||||
# BAT_API_SOCKET=/tmp/bat-pid/bat.sock
|
||||
|
||||
# Optional release root override (local fixtures / emergency read-only diagnostics only).
|
||||
# Production obtains resource_root from BAT_API_SOCKET RPC; do not set this there.
|
||||
# BAT_API_RESOURCE_ROOT=
|
||||
|
||||
# BAT_API_SERVER_INFO_FILE=
|
||||
BAT_API_REQUIRE_INDEXED=true
|
||||
BAT_API_VERIFY_SIZE=true
|
||||
BAT_API_RPC_TIMEOUT=30s
|
||||
# Periodically re-read bat.sock so bat-api follows Rust bat release switches.
|
||||
# Set to 0 in fixture-only local development.
|
||||
BAT_API_REFRESH_INTERVAL=1m
|
||||
|
||||
# Reserved for future API persistence
|
||||
# BAT_API_DATABASE_URL=postgres://bat:@127.0.0.1:5432/bat?sslmode=disable
|
||||
# BAT_API_DATABASE_PASSWORD=
|
||||
# BAT_API_REDIS_URL=redis://127.0.0.1:6379/0
|
||||
# BAT_API_REDIS_PASSWORD=
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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 (.env / 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"
|
||||
"fmt"
|
||||
"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 os.Getenv("BAT_API_SKIP_ENV_FILE") != "1" {
|
||||
envPath := envFilePath()
|
||||
if err := ensureEnvTemplate(envPath); err != nil {
|
||||
log.Printf("warn: env template: %v", err)
|
||||
}
|
||||
if err := api.LoadEnvFile(envPath); err != nil {
|
||||
log.Fatalf("load .env: %v", err)
|
||||
}
|
||||
}
|
||||
api.ApplyEnv(&cfg)
|
||||
|
||||
listen := flag.String("listen", cfg.Listen, "HTTP listen address")
|
||||
publicBase := flag.String("public-base-url", cfg.PublicBaseURL, "public base URL for Addressables rewrite")
|
||||
stateDir := flag.String("state-dir", cfg.StateDir, "bat daemon state dir (derives default socket)")
|
||||
socket := flag.String("socket", cfg.SocketPath, "path to bat.sock JSON-RPC socket (primary discovery)")
|
||||
resourceRoot := flag.String("resource-root", cfg.ResourceRoot, "override published release root (tests/emergency)")
|
||||
serverInfo := flag.String("server-info-file", cfg.ServerInfoFile, "optional server-info JSON path")
|
||||
requireIndexed := flag.Bool("require-indexed", cfg.RequireIndexed, "only serve files present in the release index")
|
||||
verifySize := flag.Bool("verify-size", cfg.VerifySize, "reject CDN files whose size differs from the index")
|
||||
rpcTimeout := flag.Duration("rpc-timeout", cfg.RPCTimeout, "daemon RPC timeout")
|
||||
refreshInterval := flag.Duration("refresh-interval", cfg.RefreshInterval, "periodic release discovery interval (0 disables)")
|
||||
authQueryParam := flag.String("auth-query-param", cfg.AuthQueryParam, "query parameter accepted for token auth fallback")
|
||||
authExemptPaths := flag.String("auth-exempt-paths", strings.Join(cfg.AuthExemptPaths, ","), "comma-separated auth-exempt exact paths or slash-prefixes")
|
||||
trustProxyHeaders := flag.Bool("trust-proxy-headers", cfg.TrustProxyHeaders, "trust X-Forwarded-For and X-Real-IP from reverse proxy")
|
||||
accessLog := flag.Bool("access-log", cfg.AccessLog, "enable per-request access logs without query strings")
|
||||
rateLimitRPS := flag.Float64("rate-limit-rps", cfg.RateLimitRPS, "per-client request rate limit; 0 disables")
|
||||
rateLimitBurst := flag.Int("rate-limit-burst", cfg.RateLimitBurst, "per-client rate limit burst")
|
||||
maxResourceLimit := flag.Int("max-resource-limit", cfg.MaxResourcePageLimit, "maximum /v1/resources page size")
|
||||
flag.Parse()
|
||||
|
||||
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.AuthQueryParam = *authQueryParam
|
||||
cfg.AuthExemptPaths = splitFlagCSV(*authExemptPaths)
|
||||
cfg.TrustProxyHeaders = *trustProxyHeaders
|
||||
cfg.AccessLog = *accessLog
|
||||
cfg.RateLimitRPS = *rateLimitRPS
|
||||
cfg.RateLimitBurst = *rateLimitBurst
|
||||
cfg.MaxResourcePageLimit = *maxResourceLimit
|
||||
// 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 envFilePath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return api.EnvFileName
|
||||
}
|
||||
return filepath.Join(filepath.Dir(exe), api.EnvFileName)
|
||||
}
|
||||
|
||||
func ensureEnvTemplate(path string) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(api.EnvTemplate), 0o600); err != nil {
|
||||
return fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
log.Printf("wrote config template %s", path)
|
||||
return 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
|
||||
}
|
||||
+9
-4
@@ -34,10 +34,15 @@ func main() {
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("bat - BlueArchiveToolkit CLI")
|
||||
fmt.Println("bat-go - experimental Go helper (NOT the product CLI)")
|
||||
fmt.Println()
|
||||
fmt.Println("Product sync/ops CLI is the Rust binary `bat` (nearly fully automatic).")
|
||||
fmt.Println("Product resource HTTP service is `bat-api` (see docs/reports/GO_STATUS.md).")
|
||||
fmt.Println()
|
||||
fmt.Println("This binary is experimental FFI demos only. Build output must be bin/bat-go.")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" bat doctor")
|
||||
fmt.Println(" bat manifest inspect <file>")
|
||||
fmt.Println(" bat sync plan <current-json> [previous-json]")
|
||||
fmt.Println(" bat-go doctor")
|
||||
fmt.Println(" bat-go manifest inspect <file>")
|
||||
fmt.Println(" bat-go sync plan <current-json> [previous-json]")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user