Files

282 lines
10 KiB
Go

// Package config provides global server-wide settings.
package config
import (
"context"
"log"
"net/url"
"strings"
"time"
// TODO: figure out how to properly implement urlx
// the implementation in 5f8b659b49 causes config.go to segfault due to a nil pointer dereference when the PIXIVFE_IMAGEPROXY env var is not set
// "github.com/goware/urlx"
"github.com/sethvargo/go-envconfig"
"codeberg.org/vnpower/pixivfe/v2/i18n"
"codeberg.org/vnpower/pixivfe/v2/server/token_manager"
"codeberg.org/vnpower/pixivfe/v2/server/utils"
)
var GlobalConfig ServerConfig
// revision stores the current version's revision information
var revision string = ""
const (
unknownRevision = "unknown"
revisionFormat = "date-hash[+dirty]"
version = "v2.10"
defaultPort = "8282"
defaultTimeZone = "UTC"
defaultRepoUrl = "https://codeberg.org/PixivFE/PixivFE"
defaultAcceptLanguage = "en-US,en;q=0.5"
defaultProxyServerStaging = BuiltinProxyUrl
defaultProxyCheckEnabled = true
defaultProxyCheckInterval = 8 * time.Hour
defaultProxyCheckTimeout = 4 * time.Second
defaultTokenLoadBalancing = "round-robin"
defaultTokenMaxRetries = 5
defaultTokenBaseTimeout = 1000 * time.Millisecond
defaultTokenMaxBackoffTime = 32000 * time.Millisecond
defaultCacheEnabled = false
defaultCacheSize = 100
defaultCacheTTL = 60 * time.Minute
defaultCacheShortTTL = 10 * time.Second
defaultCacheControlMaxAge = 30 * time.Second
defaultCacheControlStaleWhileRevalidate = 60 * time.Second
defaultPopularSearchEnabled = false
defaultResponseSaveLocation = "/tmp/pixivfe/responses"
defaultLogLevel = "info"
defaultLogFormat = "console"
)
var defaultLogOutputs = []string{"stdout"}
type ServerConfig struct {
Version string
Revision string
RevisionDate string
RevisionHash string
IsDirty bool
StartingTime string // used in /about page
Host string `env:"PIXIVFE_HOST"`
// One of the two is required
Port string `env:"PIXIVFE_PORT"`
UnixSocket string `env:"PIXIVFE_UNIXSOCKET"`
RepoURL string `env:"PIXIVFE_REPO_URL,overwrite"` // used in /about page
Token []string `env:"PIXIVFE_TOKEN,required"` // may be multiple tokens. delimiter is ','
TokenManager *token_manager.TokenManager
TokenLoadBalancing string `env:"PIXIVFE_TOKEN_LOAD_BALANCING,overwrite"`
TokenMaxRetries int `env:"PIXIVFE_TOKEN_MAX_RETRIES,overwrite"`
TokenBaseTimeout time.Duration `env:"PIXIVFE_TOKEN_BASE_TIMEOUT,overwrite"`
TokenMaxBackoffTime time.Duration `env:"PIXIVFE_TOKEN_MAX_BACKOFF_TIME,overwrite"`
AcceptLanguage string `env:"PIXIVFE_ACCEPTLANGUAGE,overwrite"`
RequestLimit uint64 `env:"PIXIVFE_REQUESTLIMIT"` // if 0, request limit is disabled
ProxyServer_staging string `env:"PIXIVFE_IMAGEPROXY,overwrite"`
ProxyServer url.URL // proxy server URL, may or may not contain authority part of the URL
ProxyCheckEnabled bool `env:"PIXIVFE_PROXY_CHECK_ENABLED,overwrite"`
ProxyCheckInterval time.Duration `env:"PIXIVFE_PROXY_CHECK_INTERVAL,overwrite"`
ProxyCheckTimeout time.Duration `env:"PIXIVFE_PROXY_CHECK_TIMEOUT,overwrite"`
// API response caching configuration
CacheEnabled bool `env:"PIXIVFE_CACHE_ENABLED,overwrite"`
CacheSize int `env:"PIXIVFE_CACHE_SIZE,overwrite"`
CacheTTL time.Duration `env:"PIXIVFE_CACHE_TTL,overwrite"`
CacheShortTTL time.Duration `env:"PIXIVFE_CACHE_SHORT_TTL,overwrite"`
// Cache-Control header configuration
// TODO: fine-tune the default durations to actual usage patterns
CacheControlMaxAge time.Duration `env:"PIXIVFE_CACHE_CONTROL_MAX_AGE,overwrite"`
CacheControlStaleWhileRevalidate time.Duration `env:"PIXIVFE_CACHE_CONTROL_STALE_WHILE_REVALIDATE,overwrite"`
// Feature configuration
PopularSearchEnabled bool `env:"PIXIVFE_POPULAR_SEARCH_ENABLED,overwrite"`
// Development options
InDevelopment bool `env:"PIXIVFE_DEV"`
ResponseSaveLocation string `env:"PIXIVFE_RESPONSE_SAVE_LOCATION,overwrite"`
// Logging configuration
LogLevel string `env:"PIXIVFE_LOG_LEVEL,overwrite"`
LogOutputs []string `env:"PIXIVFE_LOG_OUTPUTS,overwrite"`
LogFormat string `env:"PIXIVFE_LOG_FORMAT,overwrite"`
}
// parseRevision extracts RevisionDate, RevisionHash, and IsDirty status from the Revision string
func parseRevision(revision string) (date, hash string, isDirty bool) {
// Check if Revision is empty
if revision == "" {
return unknownRevision, unknownRevision, false
}
// Check if Revision is marked as dirty (has uncommitted changes)
isDirty = strings.HasSuffix(revision, "+dirty")
if isDirty {
revision = strings.TrimSuffix(revision, "+dirty")
}
// Split Revision into two parts, RevisionDate and RevisionHash
parts := strings.Split(revision, "-")
if len(parts) == 2 {
return parts[0], parts[1], isDirty
}
// Return unknown date, full string as hash if format doesn't match date-hash
return unknownRevision, revision, isDirty
}
func (s *ServerConfig) LoadConfig() error {
s.Version = version
s.Revision = revision
s.RevisionDate, s.RevisionHash, s.IsDirty = parseRevision(revision)
if revision == "" {
log.Printf("[WARNING] REVISION is not set. Continuing with unknown revision information.\n")
} else if s.RevisionDate == unknownRevision {
log.Printf("[WARNING] REVISION format is invalid: %s. Expected format '%s'. Continuing with full revision as hash.\n", revision, revisionFormat)
}
log.Printf("PixivFE %s, revision %s\n", s.Version, s.Revision)
s.StartingTime = time.Now().UTC().Format("2006-01-02 15:04")
// set default values with env:"...,overwrite"
s.RepoURL = defaultRepoUrl
s.AcceptLanguage = defaultAcceptLanguage
s.ProxyServer_staging = defaultProxyServerStaging
s.ProxyCheckEnabled = defaultProxyCheckEnabled
s.ProxyCheckInterval = defaultProxyCheckInterval
s.ProxyCheckTimeout = defaultProxyCheckTimeout
s.TokenLoadBalancing = defaultTokenLoadBalancing
s.TokenMaxRetries = defaultTokenMaxRetries
s.TokenBaseTimeout = defaultTokenBaseTimeout
s.TokenMaxBackoffTime = defaultTokenMaxBackoffTime
s.CacheEnabled = defaultCacheEnabled
s.CacheSize = defaultCacheSize
s.CacheTTL = defaultCacheTTL
s.CacheShortTTL = defaultCacheShortTTL
s.CacheControlMaxAge = defaultCacheControlMaxAge
s.CacheControlStaleWhileRevalidate = defaultCacheControlStaleWhileRevalidate
s.PopularSearchEnabled = defaultPopularSearchEnabled
s.ResponseSaveLocation = defaultResponseSaveLocation
s.LogLevel = defaultLogLevel
s.LogOutputs = defaultLogOutputs
s.LogFormat = defaultLogFormat
// load config from from env vars
if err := envconfig.Process(context.Background(), s); err != nil {
return err
}
// Check if either Port or UnixSocket is set, if not, use defaultPort
if s.Port == "" && s.UnixSocket == "" {
s.Port = defaultPort
log.Printf("[INFO] Neither PIXIVFE_PORT nor PIXIVFE_UNIXSOCKET set. Using default port: %s\n", s.Port)
}
// Check for when both Host and UnixSocket are set
if s.Host != "" && s.UnixSocket != "" {
log.Printf("[WARNING] Both PIXIVFE_HOST and PIXIVFE_UNIXSOCKET are set. PIXIVFE_HOST will be ignored when using a Unix socket.\n")
}
// a check for tokens
if len(s.Token) < 1 {
log.Fatalln("PIXIVFE_TOKEN has to be set. Visit https://pixivfe-docs.pages.dev/hosting/hosting-pixivfe for more details.")
return i18n.Error("PIXIVFE_TOKEN has to be set. Visit https://pixivfe-docs.pages.dev/hosting/hosting-pixivfe for more details")
}
// Validate proxy server URL, but skip validation if it already points to the built-in proxy
if s.ProxyServer_staging == BuiltinProxyUrl {
proxyURL, _ := url.Parse(BuiltinProxyUrl)
s.ProxyServer = *proxyURL
log.Printf("Using built-in proxy server URL: %s\n", BuiltinProxyUrl)
} else {
proxyURL, err := utils.ValidateURL(s.ProxyServer_staging, "Proxy server")
if err != nil {
log.Printf("[WARNING] Invalid proxy server URL: %v. Falling back to built-in proxy URL.", err)
proxyURL, _ := url.Parse(BuiltinProxyUrl)
s.ProxyServer = *proxyURL
log.Printf("Proxy server set to: %s\n", BuiltinProxyUrl)
} else {
s.ProxyServer = *proxyURL
log.Printf("Proxy server set to: %s\n", proxyURL.String())
}
}
log.Printf("Proxy check interval set to: %v\n", s.ProxyCheckInterval)
// Validate repo URL
repoURL, err := utils.ValidateURL(s.RepoURL, "Repo")
if err != nil {
log.Printf("[WARNING] Invalid repo URL: %v. Using default repo URL.", err)
s.RepoURL = "https://codeberg.org/VnPower/PixivFE" // Use a default value
} else {
s.RepoURL = repoURL.String()
log.Printf("Repo URL set to: %s\n", s.RepoURL)
}
// Validate TokenLoadBalancing
switch s.TokenLoadBalancing {
case "round-robin", "random", "least-recently-used":
// Valid options
default:
log.Printf("[WARNING] Invalid PIXIVFE_TOKEN_LOAD_BALANCING value: %s. Defaulting to 'round-robin'.\n", s.TokenLoadBalancing)
s.TokenLoadBalancing = "round-robin"
}
// Initialize TokenManager
s.TokenManager = token_manager.NewTokenManager(s.Token, s.TokenMaxRetries, s.TokenBaseTimeout, s.TokenMaxBackoffTime, s.TokenLoadBalancing)
log.Printf("Token manager initialized with %d tokens\n", len(s.Token))
log.Printf("Token manager settings: Max retries: %d, Base timeout: %v, Max backoff time: %v\n", s.TokenMaxRetries, s.TokenBaseTimeout, s.TokenMaxBackoffTime)
log.Printf("Token load balancing method: %s\n", s.TokenLoadBalancing)
// Print cache configuration
if s.CacheEnabled {
log.Println("API response cache is enabled")
} else {
log.Println("API response cache is disabled")
}
if s.PopularSearchEnabled {
log.Println("Tag search by popularity is enabled")
} else {
log.Println("Tag search by popularity is disabled")
}
// Only print ResponseSaveLocation if InDevelopment is set
if s.InDevelopment {
log.Printf("Response save location: %s\n", s.ResponseSaveLocation)
}
return nil
}
func (s *ServerConfig) GetToken() string {
token := s.TokenManager.GetToken()
if token == nil {
log.Println("[WARNING] All tokens are timed out. Using the first available token.")
return s.Token[0]
}
return token.Value
}