253 lines
5.8 KiB
Go
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
|
|
}
|
|
}
|