first commit
This commit is contained in:
252
backend/internal/config/config.go
Normal file
252
backend/internal/config/config.go
Normal file
@@ -0,0 +1,252 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user