- app_settings table (PostgreSQL migration 000010, SQLite 000002) - internal/settings repository (Get/Set/Ensure) wired into store.Repos - secret generated with crypto/rand on first start and persisted; reused on restart - drop required JWT_SECRET from env/config; app.New takes a TokenManager and storage.Open takes the resolved secret (URL_SECRET still overrides) - docs updated
206 lines
4.6 KiB
Go
206 lines
4.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
type Postgres struct {
|
|
Host string
|
|
Port int
|
|
User string
|
|
Password string
|
|
Database string
|
|
SSLMode string
|
|
}
|
|
|
|
func (p Postgres) DSN() string {
|
|
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
|
p.User, p.Password, p.Host, p.Port, p.Database, p.SSLMode)
|
|
}
|
|
|
|
type S3 struct {
|
|
Endpoint string
|
|
Region string
|
|
AccessKey string
|
|
SecretKey string
|
|
Bucket string
|
|
UsePathStyle bool
|
|
}
|
|
|
|
type Auth struct {
|
|
AccessTTL time.Duration
|
|
RefreshTTL time.Duration
|
|
}
|
|
|
|
type Server struct {
|
|
Addr string
|
|
BaseURL string
|
|
}
|
|
|
|
type SSE struct {
|
|
HeartbeatInterval time.Duration
|
|
ClientBuffer int
|
|
}
|
|
|
|
type Worker struct {
|
|
Interval time.Duration
|
|
Batch int
|
|
PendingTTL time.Duration
|
|
}
|
|
|
|
type Storage struct {
|
|
Backend string
|
|
FSRoot string
|
|
URLSecret string
|
|
URLTTL time.Duration
|
|
}
|
|
|
|
type Database struct {
|
|
Backend string
|
|
SQLitePath string
|
|
}
|
|
|
|
type Config struct {
|
|
Postgres Postgres
|
|
S3 S3
|
|
Storage Storage
|
|
Database Database
|
|
Auth Auth
|
|
Server Server
|
|
SSE SSE
|
|
Worker Worker
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
pgPort, err := strconv.Atoi(getEnv("PG_PORT", "5432"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid PG_PORT: %w", err)
|
|
}
|
|
|
|
accessTTL, err := time.ParseDuration(getEnv("JWT_ACCESS_TTL", "15m"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid JWT_ACCESS_TTL: %w", err)
|
|
}
|
|
|
|
refreshTTL, err := time.ParseDuration(getEnv("JWT_REFRESH_TTL", "720h"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid JWT_REFRESH_TTL: %w", err)
|
|
}
|
|
|
|
urlTTL, err := time.ParseDuration(getEnv("OBJECT_URL_TTL", "1h"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid OBJECT_URL_TTL: %w", err)
|
|
}
|
|
|
|
heartbeat, err := time.ParseDuration(getEnv("SSE_HEARTBEAT_INTERVAL", "25s"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SSE_HEARTBEAT_INTERVAL: %w", err)
|
|
}
|
|
|
|
bufSize, err := strconv.Atoi(getEnv("SSE_CLIENT_BUFFER", "64"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SSE_CLIENT_BUFFER: %w", err)
|
|
}
|
|
|
|
pathStyle, err := strconv.ParseBool(getEnv("S3_USE_PATH_STYLE", "true"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid S3_USE_PATH_STYLE: %w", err)
|
|
}
|
|
|
|
workerInterval, err := time.ParseDuration(getEnv("WORKER_INTERVAL", "5s"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid WORKER_INTERVAL: %w", err)
|
|
}
|
|
|
|
workerBatch, err := strconv.Atoi(getEnv("WORKER_BATCH", "10"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid WORKER_BATCH: %w", err)
|
|
}
|
|
|
|
workerPendingTTL, err := time.ParseDuration(getEnv("WORKER_PENDING_TTL", "10m"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid WORKER_PENDING_TTL: %w", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
Postgres: Postgres{
|
|
Host: getEnv("PG_HOST", "pg.localnet"),
|
|
Port: pgPort,
|
|
User: getEnv("PG_USER", "album"),
|
|
Password: getEnv("PG_PASSWORD", ""),
|
|
Database: getEnv("PG_DATABASE", "shared_album"),
|
|
SSLMode: getEnv("PG_SSLMODE", "disable"),
|
|
},
|
|
S3: S3{
|
|
Endpoint: getEnv("S3_ENDPOINT", "http://s3.localnet:3900"),
|
|
Region: getEnv("S3_REGION", "garage"),
|
|
AccessKey: getEnv("S3_ACCESS_KEY", ""),
|
|
SecretKey: getEnv("S3_SECRET_KEY", ""),
|
|
Bucket: getEnv("S3_BUCKET", "albums"),
|
|
UsePathStyle: pathStyle,
|
|
},
|
|
Storage: Storage{
|
|
Backend: getEnv("STORAGE_BACKEND", "fs"),
|
|
FSRoot: getEnv("FS_ROOT", "./data"),
|
|
URLSecret: getEnv("URL_SECRET", ""),
|
|
URLTTL: urlTTL,
|
|
},
|
|
Database: Database{
|
|
Backend: getEnv("DB_BACKEND", "sqlite"),
|
|
SQLitePath: getEnv("SQLITE_PATH", "./galo.db"),
|
|
},
|
|
Auth: Auth{
|
|
AccessTTL: accessTTL,
|
|
RefreshTTL: refreshTTL,
|
|
},
|
|
Server: Server{
|
|
Addr: getEnv("HTTP_ADDR", ":8080"),
|
|
BaseURL: getEnv("APP_BASE_URL", "http://localhost:8080"),
|
|
},
|
|
SSE: SSE{
|
|
HeartbeatInterval: heartbeat,
|
|
ClientBuffer: bufSize,
|
|
},
|
|
Worker: Worker{
|
|
Interval: workerInterval,
|
|
Batch: workerBatch,
|
|
PendingTTL: workerPendingTTL,
|
|
},
|
|
}
|
|
|
|
switch cfg.Database.Backend {
|
|
case "postgres":
|
|
if cfg.Postgres.Password == "" {
|
|
return nil, fmt.Errorf("PG_PASSWORD is required")
|
|
}
|
|
case "sqlite":
|
|
if cfg.Database.SQLitePath == "" {
|
|
return nil, fmt.Errorf("SQLITE_PATH is required when DB_BACKEND=sqlite")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("invalid DB_BACKEND %q (want postgres or sqlite)", cfg.Database.Backend)
|
|
}
|
|
|
|
switch cfg.Storage.Backend {
|
|
case "s3":
|
|
case "fs":
|
|
if cfg.Storage.FSRoot == "" {
|
|
return nil, fmt.Errorf("FS_ROOT is required when STORAGE_BACKEND=fs")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("invalid STORAGE_BACKEND %q (want s3 or fs)", cfg.Storage.Backend)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|