mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
feat: impl config loading
This commit is contained in:
@@ -1,19 +1,27 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/get-drexa/drexa/internal/drexa"
|
"github.com/get-drexa/drexa/internal/drexa"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load()
|
configPath := flag.String("config", "", "path to config file (required)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
config, err := drexa.ServerConfigFromEnv()
|
if *configPath == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "error: --config is required")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := drexa.ConfigFromFile(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := drexa.NewServer(*config)
|
server, err := drexa.NewServer(*config)
|
||||||
@@ -21,5 +29,6 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(server.Listen(fmt.Sprintf(":%d", config.Port)))
|
log.Printf("starting server on :%d", config.Server.Port)
|
||||||
|
log.Fatal(server.Listen(fmt.Sprintf(":%d", config.Server.Port)))
|
||||||
}
|
}
|
||||||
|
|||||||
30
apps/backend/config.example.yaml
Normal file
30
apps/backend/config.example.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Drexa Backend Configuration
|
||||||
|
# Copy this file to config.yaml and adjust values for your environment.
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
database:
|
||||||
|
postgres_url: postgres://user:password@localhost:5432/drexa?sslmode=disable
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
issuer: drexa
|
||||||
|
audience: drexa-api
|
||||||
|
# Secret key can be provided via (in order of precedence):
|
||||||
|
# 1. JWT_SECRET_KEY environment variable (base64 encoded)
|
||||||
|
# 2. secret_key_base64 below (base64 encoded)
|
||||||
|
# 3. secret_key_path below (file with base64 encoded content)
|
||||||
|
# secret_key_base64: "base64encodedkey"
|
||||||
|
secret_key_path: /run/secrets/jwt_secret_key
|
||||||
|
|
||||||
|
storage:
|
||||||
|
# Mode: "flat" (UUID-based keys) or "hierarchical" (path-based keys)
|
||||||
|
# Note: S3 backend only supports "flat" mode
|
||||||
|
mode: flat
|
||||||
|
# Backend: "fs" (filesystem) or "s3" (not yet implemented)
|
||||||
|
backend: fs
|
||||||
|
# Required when backend is "fs"
|
||||||
|
root_path: /var/lib/drexa/blobs
|
||||||
|
# Required when backend is "s3"
|
||||||
|
# bucket: my-drexa-bucket
|
||||||
|
|
||||||
@@ -3,16 +3,17 @@ module github.com/get-drexa/drexa
|
|||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11
|
||||||
github.com/gofiber/fiber/v2 v2.52.9
|
github.com/gofiber/fiber/v2 v2.52.9
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/sqids/sqids-go v0.4.1
|
||||||
github.com/uptrace/bun v1.2.15
|
github.com/uptrace/bun v1.2.15
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
github.com/sqids/sqids-go v0.4.1 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
mellium.im/sasl v0.3.2 // indirect
|
mellium.im/sasl v0.3.2 // indirect
|
||||||
@@ -20,7 +21,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
@@ -28,7 +29,6 @@ require (
|
|||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.15
|
github.com/uptrace/bun/dialect/pgdialect v1.2.15
|
||||||
github.com/uptrace/bun/driver/pgdriver v1.2.15
|
github.com/uptrace/bun/driver/pgdriver v1.2.15
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5
|
|||||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -16,12 +18,16 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
@@ -59,6 +65,9 @@ golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+Zdx
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
||||||
|
|||||||
155
apps/backend/internal/drexa/config.go
Normal file
155
apps/backend/internal/drexa/config.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package drexa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageMode string
|
||||||
|
type StorageBackend string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StorageModeFlat StorageMode = "flat"
|
||||||
|
StorageModeHierarchical StorageMode = "hierarchical"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StorageBackendFS StorageBackend = "fs"
|
||||||
|
StorageBackendS3 StorageBackend = "s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
JWT JWTConfig `yaml:"jwt"`
|
||||||
|
Storage StorageConfig `yaml:"storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
PostgresURL string `yaml:"postgres_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTConfig struct {
|
||||||
|
Issuer string `yaml:"issuer"`
|
||||||
|
Audience string `yaml:"audience"`
|
||||||
|
SecretKeyBase64 string `yaml:"secret_key_base64"`
|
||||||
|
SecretKeyPath string `yaml:"secret_key_path"`
|
||||||
|
SecretKey []byte `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageConfig struct {
|
||||||
|
Mode StorageMode `yaml:"mode"`
|
||||||
|
Backend StorageBackend `yaml:"backend"`
|
||||||
|
RootPath string `yaml:"root_path"`
|
||||||
|
Bucket string `yaml:"bucket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigFromFile loads configuration from a YAML file.
|
||||||
|
// JWT secret key is loaded from JWT_SECRET_KEY env var (base64 encoded),
|
||||||
|
// falling back to the file path specified in jwt.secret_key_path.
|
||||||
|
func ConfigFromFile(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("config file not found: %s", path)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load JWT secret key (priority: env var > config base64 > config file path)
|
||||||
|
if envKey := os.Getenv("JWT_SECRET_KEY"); envKey != "" {
|
||||||
|
key, err := base64.StdEncoding.DecodeString(envKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("JWT_SECRET_KEY env var is not valid base64")
|
||||||
|
}
|
||||||
|
config.JWT.SecretKey = key
|
||||||
|
} else if config.JWT.SecretKeyBase64 != "" {
|
||||||
|
key, err := base64.StdEncoding.DecodeString(config.JWT.SecretKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("jwt.secret_key_base64 is not valid base64")
|
||||||
|
}
|
||||||
|
config.JWT.SecretKey = key
|
||||||
|
} else if config.JWT.SecretKeyPath != "" {
|
||||||
|
keyData, err := os.ReadFile(config.JWT.SecretKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, err := base64.StdEncoding.DecodeString(string(keyData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("jwt.secret_key_path file content is not valid base64")
|
||||||
|
}
|
||||||
|
config.JWT.SecretKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs := config.Validate(); len(errs) > 0 {
|
||||||
|
return nil, NewConfigError(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks for required configuration fields.
|
||||||
|
func (c *Config) Validate() []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// Server
|
||||||
|
if c.Server.Port == 0 {
|
||||||
|
errs = append(errs, errors.New("server.port is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database
|
||||||
|
if c.Database.PostgresURL == "" {
|
||||||
|
errs = append(errs, errors.New("database.postgres_url is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
if c.JWT.Issuer == "" {
|
||||||
|
errs = append(errs, errors.New("jwt.issuer is required"))
|
||||||
|
}
|
||||||
|
if c.JWT.Audience == "" {
|
||||||
|
errs = append(errs, errors.New("jwt.audience is required"))
|
||||||
|
}
|
||||||
|
if len(c.JWT.SecretKey) == 0 {
|
||||||
|
errs = append(errs, errors.New("jwt secret key is required (set JWT_SECRET_KEY env var, jwt.secret_key_base64, or jwt.secret_key_path)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
if c.Storage.Mode == "" {
|
||||||
|
errs = append(errs, errors.New("storage.mode is required"))
|
||||||
|
} else if c.Storage.Mode != StorageModeFlat && c.Storage.Mode != StorageModeHierarchical {
|
||||||
|
errs = append(errs, errors.New("storage.mode must be 'flat' or 'hierarchical'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Storage.Backend == "" {
|
||||||
|
errs = append(errs, errors.New("storage.backend is required"))
|
||||||
|
} else if c.Storage.Backend != StorageBackendFS && c.Storage.Backend != StorageBackendS3 {
|
||||||
|
errs = append(errs, errors.New("storage.backend must be 'fs' or 's3'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Storage.Backend == StorageBackendFS && c.Storage.RootPath == "" {
|
||||||
|
errs = append(errs, errors.New("storage.root_path is required when backend is 'fs'"))
|
||||||
|
}
|
||||||
|
if c.Storage.Backend == StorageBackendS3 {
|
||||||
|
if c.Storage.Bucket == "" {
|
||||||
|
errs = append(errs, errors.New("storage.bucket is required when backend is 's3'"))
|
||||||
|
}
|
||||||
|
if c.Storage.Mode == StorageModeHierarchical {
|
||||||
|
errs = append(errs, errors.New("storage.mode must be 'flat' when backend is 's3'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
@@ -5,17 +5,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfigError struct {
|
type ConfigError struct {
|
||||||
Errors []error
|
Errors []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerConfigError(errs ...error) *ServerConfigError {
|
func NewConfigError(errs ...error) *ConfigError {
|
||||||
return &ServerConfigError{Errors: errs}
|
return &ConfigError{Errors: errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ServerConfigError) Error() string {
|
func (e *ConfigError) Error() string {
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
sb.WriteString("invalid server config:\n")
|
sb.WriteString("invalid config:\n")
|
||||||
for _, err := range e.Errors {
|
for _, err := range e.Errors {
|
||||||
sb.WriteString(fmt.Sprintf(" - %s\n", err.Error()))
|
sb.WriteString(fmt.Sprintf(" - %s\n", err.Error()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package drexa
|
package drexa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/get-drexa/drexa/internal/auth"
|
"github.com/get-drexa/drexa/internal/auth"
|
||||||
"github.com/get-drexa/drexa/internal/blob"
|
"github.com/get-drexa/drexa/internal/blob"
|
||||||
@@ -16,24 +12,34 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfig struct {
|
func NewServer(c Config) (*fiber.App, error) {
|
||||||
Port int
|
|
||||||
PostgresURL string
|
|
||||||
JWTIssuer string
|
|
||||||
JWTAudience string
|
|
||||||
JWTSecretKey []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(c ServerConfig) (*fiber.App, error) {
|
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
db := database.NewFromPostgres(c.PostgresURL)
|
db := database.NewFromPostgres(c.Database.PostgresURL)
|
||||||
|
|
||||||
// TODO: load correct blob store and resolver from config
|
// Initialize blob store based on config
|
||||||
blobStore := blob.NewFSStore(blob.FSStoreConfig{
|
var blobStore blob.Store
|
||||||
Root: os.Getenv("BLOB_ROOT"),
|
switch c.Storage.Backend {
|
||||||
UploadURL: os.Getenv("BLOB_UPLOAD_URL"),
|
case StorageBackendFS:
|
||||||
|
blobStore = blob.NewFSStore(blob.FSStoreConfig{
|
||||||
|
Root: c.Storage.RootPath,
|
||||||
})
|
})
|
||||||
keyResolver := virtualfs.NewFlatKeyResolver()
|
case StorageBackendS3:
|
||||||
|
return nil, fmt.Errorf("s3 storage backend not yet implemented")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown storage backend: %s", c.Storage.Backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize key resolver based on config
|
||||||
|
var keyResolver virtualfs.BlobKeyResolver
|
||||||
|
switch c.Storage.Mode {
|
||||||
|
case StorageModeFlat:
|
||||||
|
keyResolver = virtualfs.NewFlatKeyResolver()
|
||||||
|
case StorageModeHierarchical:
|
||||||
|
keyResolver = virtualfs.NewHierarchicalKeyResolver(db)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown storage mode: %s", c.Storage.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
vfs, err := virtualfs.NewVirtualFS(db, blobStore, keyResolver)
|
vfs, err := virtualfs.NewVirtualFS(db, blobStore, keyResolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create virtual file system: %w", err)
|
return nil, fmt.Errorf("failed to create virtual file system: %w", err)
|
||||||
@@ -41,9 +47,9 @@ func NewServer(c ServerConfig) (*fiber.App, error) {
|
|||||||
|
|
||||||
userService := user.NewService(db)
|
userService := user.NewService(db)
|
||||||
authService := auth.NewService(db, userService, auth.TokenConfig{
|
authService := auth.NewService(db, userService, auth.TokenConfig{
|
||||||
Issuer: c.JWTIssuer,
|
Issuer: c.JWT.Issuer,
|
||||||
Audience: c.JWTAudience,
|
Audience: c.JWT.Audience,
|
||||||
SecretKey: c.JWTSecretKey,
|
SecretKey: c.JWT.SecretKey,
|
||||||
})
|
})
|
||||||
uploadService := upload.NewService(vfs, blobStore)
|
uploadService := upload.NewService(vfs, blobStore)
|
||||||
|
|
||||||
@@ -53,50 +59,3 @@ func NewServer(c ServerConfig) (*fiber.App, error) {
|
|||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfigFromEnv creates a ServerConfig from environment variables.
|
|
||||||
func ServerConfigFromEnv() (*ServerConfig, error) {
|
|
||||||
c := ServerConfig{
|
|
||||||
PostgresURL: os.Getenv("POSTGRES_URL"),
|
|
||||||
JWTIssuer: os.Getenv("JWT_ISSUER"),
|
|
||||||
JWTAudience: os.Getenv("JWT_AUDIENCE"),
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := []error{}
|
|
||||||
|
|
||||||
keyHex := os.Getenv("JWT_SECRET_KEY")
|
|
||||||
if keyHex == "" {
|
|
||||||
errs = append(errs, errors.New("JWT_SECRET_KEY is required"))
|
|
||||||
} else {
|
|
||||||
k, err := hex.DecodeString(keyHex)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("failed to decode JWT_SECRET_KEY: %w", err))
|
|
||||||
}
|
|
||||||
c.JWTSecretKey = k
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := strconv.Atoi(os.Getenv("PORT"))
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("failed to parse PORT: %w", err))
|
|
||||||
}
|
|
||||||
c.Port = p
|
|
||||||
|
|
||||||
if c.PostgresURL == "" {
|
|
||||||
errs = append(errs, errors.New("POSTGRES_URL is required"))
|
|
||||||
}
|
|
||||||
if c.JWTIssuer == "" {
|
|
||||||
errs = append(errs, errors.New("JWT_ISSUER is required"))
|
|
||||||
}
|
|
||||||
if c.JWTAudience == "" {
|
|
||||||
errs = append(errs, errors.New("JWT_AUDIENCE is required"))
|
|
||||||
}
|
|
||||||
if len(c.JWTSecretKey) == 0 {
|
|
||||||
errs = append(errs, errors.New("JWT_SECRET_KEY is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return nil, NewServerConfigError(errs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ type HierarchicalKeyResolver struct {
|
|||||||
db *bun.DB
|
db *bun.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewHierarchicalKeyResolver(db *bun.DB) *HierarchicalKeyResolver {
|
||||||
|
return &HierarchicalKeyResolver{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HierarchicalKeyResolver) KeyMode() blob.KeyMode {
|
func (r *HierarchicalKeyResolver) KeyMode() blob.KeyMode {
|
||||||
return blob.KeyModeDerived
|
return blob.KeyModeDerived
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user