Files
filefast/backend/internal/config/config.go
2026-03-28 15:43:18 +08:00

253 lines
5.8 KiB
Go

package config
import (
"bufio"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"filefast/backend/internal/model"
)
type Config struct {
HTTPAddress string
LogLevel slog.Level
RoomTTL time.Duration
Admin AdminConfig
SQLite SQLiteConfig
Redis RedisConfig
MinIO MinIOConfig
Runtime model.RuntimeConfig
}
type AdminConfig struct {
Username string
Password string
}
type MinIOConfig struct {
Endpoint string
AccessKey string
SecretKey string
UseSSL bool
Bucket string
PresignExpiry time.Duration
Retention time.Duration
UsageAlertLevel int
}
type RedisConfig struct {
Addr string
Password string
DB int
}
type SQLiteConfig struct {
Path string
}
func Load() Config {
loadDotEnv()
retentionHours := envInt("MINIO_RETENTION_HOURS", 2)
capacityGB := envInt("MINIO_CAPACITY_GB", 120)
return Config{
HTTPAddress: envString("HTTP_ADDR", ":8080"),
LogLevel: parseLogLevel(envString("LOG_LEVEL", "info")),
RoomTTL: time.Duration(envInt("ROOM_TTL_SECONDS", 300)) * time.Second,
Admin: AdminConfig{
Username: envString("ADMIN_USERNAME", ""),
Password: envString("ADMIN_PASSWORD", ""),
},
SQLite: SQLiteConfig{
Path: envString("SQLITE_PATH", filepath.Join("backend", "data", "filefast.db")),
},
Redis: RedisConfig{
Addr: envString("REDIS_ADDR", "127.0.0.1:6379"),
Password: envString("REDIS_PASSWORD", ""),
DB: envInt("REDIS_DB", 0),
},
MinIO: MinIOConfig{
Endpoint: envString("MINIO_ENDPOINT", ""),
AccessKey: envString("MINIO_ACCESS_KEY", ""),
SecretKey: envString("MINIO_SECRET_KEY", ""),
UseSSL: envBool("MINIO_USE_SSL", false),
Bucket: envString("MINIO_BUCKET", "filefast-fallback"),
PresignExpiry: time.Duration(envInt("MINIO_PRESIGN_MINUTES", 30)) * time.Minute,
Retention: time.Duration(retentionHours) * time.Hour,
UsageAlertLevel: envInt("MINIO_USAGE_ALERT_PERCENT", 85),
},
Runtime: model.RuntimeConfig{
MaxMinIOFallbackSizeBytes: int64(envInt("MAX_MINIO_FALLBACK_GB", 10)) * 1024 * 1024 * 1024,
MinIOCapacityBytes: int64(capacityGB) * 1024 * 1024 * 1024,
MinIORetentionHours: retentionHours,
MinIOUsageAlertPercent: envInt("MINIO_USAGE_ALERT_PERCENT", 85),
P2PConnectTimeoutSec: envInt("P2P_CONNECT_TIMEOUT_SEC", 15),
TURNConnectTimeoutSec: envInt("TURN_CONNECT_TIMEOUT_SEC", 20),
MinIOFallbackEnabled: true,
TURNURLs: envCSV("TURN_URLS"),
TURNUsername: envString("TURN_USERNAME", ""),
TURNPassword: envString("TURN_PASSWORD", ""),
},
}
}
func loadDotEnv() {
for _, candidate := range dotEnvCandidates() {
if loadDotEnvFile(candidate) {
return
}
}
}
func dotEnvCandidates() []string {
cwd, err := os.Getwd()
if err != nil {
return []string{".env", filepath.Join("backend", ".env")}
}
candidates := make([]string, 0, 16)
seen := make(map[string]struct{})
current := cwd
for {
for _, name := range []string{".env", filepath.Join("backend", ".env")} {
candidate := filepath.Clean(filepath.Join(current, name))
if _, ok := seen[candidate]; ok {
continue
}
seen[candidate] = struct{}{}
candidates = append(candidates, candidate)
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
// Preserve the most local candidates first, but keep the historical relative
// fallbacks at the end for compatibility.
for _, legacy := range []string{".env", filepath.Join("backend", ".env")} {
if !slices.Contains(candidates, legacy) {
candidates = append(candidates, legacy)
}
}
return candidates
}
func loadDotEnvFile(path string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "\uFEFF"))
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "export ") {
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
if len(value) >= 2 {
if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
(strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) {
value = value[1 : len(value)-1]
}
}
_ = os.Setenv(key, value)
}
return true
}
func envString(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}
func envBool(key string, fallback bool) bool {
value := strings.TrimSpace(strings.ToLower(os.Getenv(key)))
if value == "" {
return fallback
}
return value == "1" || value == "true" || value == "yes"
}
func envCSV(key string) []string {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value != "" {
values = append(values, value)
}
}
if len(values) == 0 {
return nil
}
return values
}
func parseLogLevel(level string) slog.Level {
switch strings.ToLower(strings.TrimSpace(level)) {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}