fix(config): 完成生产配置向 config.toml 的迁移

This commit is contained in:
2026-09-19 00:39:09 +08:00
parent 4599ea32b2
commit 045d598400
26 changed files with 908 additions and 254 deletions
-32
View File
@@ -1,32 +0,0 @@
# 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=
+88
View File
@@ -0,0 +1,88 @@
# BlueArchive Toolkit shared application configuration.
# Priority: CLI flags > process environment > config.toml > built-in defaults.
# This file is the application configuration. The application never reads .env.
#
# Rust bat consumes [runtime], [resource], [localized], [repository], [network]
# and [translation.worker]. Go bat-api consumes [api]. Each binary ignores the
# other application's section.
[runtime]
state_dir = '/tmp/bat-pid'
interval_seconds = 3600
error_retry_seconds = 60
quiet_up_to_date = false
output_format = 'human'
banner = true
progress = true
tail_lines = 200
[resource]
output_root = './bat-resources'
auto_discover = true
app_version = ''
connection_group = ''
launcher_version = '1.7.2'
platforms = ['windows', 'android']
snapshot_path = ''
dry_run = false
plan = false
force = false
audit_local = true
repair = true
[resource.server_info]
kind = 'none'
value = ''
[localized]
output_root = './bat-localized'
[repository]
import_repository = false
import_cas_root = ''
import_resource_repository_path = ''
[network]
curl_command = 'curl'
proxy = 'auto'
unzip_command = 'unzip'
zip_command = 'zip'
download_concurrency = 8
[translation.worker]
provider = 'mock'
fixture = ''
translation_memory_path = ''
glossary_path = ''
concurrency = 8
max_attempts = 3
lease_seconds = 300
retry_backoff_seconds = 5
max_tasks = ''
worker_id = ''
[api]
listen = ':18080'
public_base_url = 'http://127.0.0.1:18080'
state_dir = '/tmp/bat-pid'
socket_path = ''
resource_root = ''
server_info_file = ''
require_indexed = true
verify_size = true
rpc_timeout = '30s'
refresh_interval = '1m'
auth_token = ''
auth_query_param = 'bat_token'
auth_exempt_paths = []
trust_proxy_headers = false
access_log = false
rate_limit_rps = 0
rate_limit_burst = 0
max_resource_page_limit = 1000
# Reserved for a future API persistence layer.
database_url = ''
database_password = ''
redis_url = ''
redis_password = ''
+56 -64
View File
@@ -4,8 +4,9 @@
// - 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)
// 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
@@ -16,7 +17,6 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
@@ -34,53 +34,19 @@ func main() {
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)
}
if err := api.LoadConfigFromCurrentExe(&cfg); err != nil {
log.Fatalf("load config.toml: %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
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")
@@ -110,25 +76,51 @@ func main() {
}
}
func envFilePath() string {
exe, err := os.Executable()
if err != nil {
return api.EnvFileName
}
return filepath.Join(filepath.Dir(exe), api.EnvFileName)
}
func parseFlags(args []string, cfg api.Config) (api.Config, error) {
flags := flag.NewFlagSet("bat-api", flag.ContinueOnError)
flags.SetOutput(os.Stderr)
func ensureEnvTemplate(path string) error {
if _, err := os.Stat(path); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
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
}
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
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 {
+21
View File
@@ -0,0 +1,21 @@
package main
import (
"testing"
"bat-api/internal/api"
)
func TestParseFlagsOverridesEnvironmentConfig(t *testing.T) {
cfg := api.DefaultConfig()
cfg.Listen = ":19080"
cfg.AuthToken = "env-token"
got, err := parseFlags([]string{"--listen", ":19082", "--auth-token", "cli-token"}, cfg)
if err != nil {
t.Fatal(err)
}
if got.Listen != ":19082" || got.AuthToken != "cli-token" {
t.Fatalf("CLI did not override lower-priority values: %+v", got)
}
}