Files
ds-galo/backend/internal/config/config.go
T
iu 96268ee16c feat(photos): separate object storage from metadata layer
Move physical file facts out of the photos table into an objects ledger:

- objects / photo_objects / variant_jobs tables (migration 000008)
- drop storage_key/thumb_key/preview_key/size/width/height/mime_type from
  photos (migration 000009); they are derived via joins to the original object
- album-free storage keys: objects/{id}/{role}.{ext}
- explicit GC via pending/ready/deleting states; objects deleted only when no
  photo references them
- background worker drives variant generation (durable variant_jobs) and GC
- copy/move are metadata-only; storage objects are shared between photos
- remove unused storage.DeletePrefix

Non-destructive to the external JSON API: size/dimensions/mime and URLs are
still returned, just sourced from the object layer.

OpenSpec change: media-object-layer
2026-10-10 23:31:41 +03:00

172 lines
3.9 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
PublicURL string
PresignTTL time.Duration
}
type Auth struct {
JWTSecret string
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 Config struct {
Postgres Postgres
S3 S3
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)
}
presignTTL, err := time.ParseDuration(getEnv("S3_PRESIGN_TTL", "1h"))
if err != nil {
return nil, fmt.Errorf("invalid S3_PRESIGN_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,
PublicURL: getEnv("S3_PUBLIC_URL", ""),
PresignTTL: presignTTL,
},
Auth: Auth{
JWTSecret: getEnv("JWT_SECRET", ""),
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,
},
}
if cfg.Auth.JWTSecret == "" {
return nil, fmt.Errorf("JWT_SECRET is required")
}
if cfg.Postgres.Password == "" {
return nil, fmt.Errorf("PG_PASSWORD is required")
}
return cfg, nil
}
func getEnv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok && v != "" {
return v
}
return fallback
}