docs: expand configuration docs for admin and BadgerDB
Update README to explain startup config precedence (defaults/env/admin overrides), document admin/bootstrap and feature toggles, and clarify storage locations under WARPBOX_DATA_DIR including BadgerDB metadata. Also refresh project layout to include new config and metastore packages.docs: expand configuration docs for admin and BadgerDB Update README to explain startup config precedence (defaults/env/admin overrides), document admin/bootstrap and feature toggles, and clarify storage locations under WARPBOX_DATA_DIR including BadgerDB metadata. Also refresh project layout to include new config and metastore packages.
This commit is contained in:
523
lib/config/config.go
Normal file
523
lib/config/config.go
Normal file
@@ -0,0 +1,523 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Source string
|
||||
|
||||
const (
|
||||
SourceDefault Source = "default"
|
||||
SourceEnv Source = "environment"
|
||||
SourceDB Source = "db override"
|
||||
)
|
||||
|
||||
type AdminEnabledMode string
|
||||
|
||||
const (
|
||||
AdminEnabledAuto AdminEnabledMode = "auto"
|
||||
AdminEnabledTrue AdminEnabledMode = "true"
|
||||
AdminEnabledFalse AdminEnabledMode = "false"
|
||||
)
|
||||
|
||||
const (
|
||||
SettingGuestUploadsEnabled = "guest_uploads_enabled"
|
||||
SettingAPIEnabled = "api_enabled"
|
||||
SettingZipDownloadsEnabled = "zip_downloads_enabled"
|
||||
SettingOneTimeDownloadsEnabled = "one_time_downloads_enabled"
|
||||
SettingRenewOnAccessEnabled = "renew_on_access_enabled"
|
||||
SettingRenewOnDownloadEnabled = "renew_on_download_enabled"
|
||||
SettingDefaultGuestExpirySecs = "default_guest_expiry_seconds"
|
||||
SettingMaxGuestExpirySecs = "max_guest_expiry_seconds"
|
||||
SettingGlobalMaxFileSizeBytes = "global_max_file_size_bytes"
|
||||
SettingGlobalMaxBoxSizeBytes = "global_max_box_size_bytes"
|
||||
SettingDefaultUserMaxFileBytes = "default_user_max_file_size_bytes"
|
||||
SettingDefaultUserMaxBoxBytes = "default_user_max_box_size_bytes"
|
||||
SettingSessionTTLSeconds = "session_ttl_seconds"
|
||||
SettingBoxPollIntervalMS = "box_poll_interval_ms"
|
||||
SettingThumbnailBatchSize = "thumbnail_batch_size"
|
||||
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
|
||||
SettingDataDir = "data_dir"
|
||||
)
|
||||
|
||||
type SettingType string
|
||||
|
||||
const (
|
||||
SettingTypeBool SettingType = "bool"
|
||||
SettingTypeInt64 SettingType = "int64"
|
||||
SettingTypeInt SettingType = "int"
|
||||
SettingTypeText SettingType = "text"
|
||||
)
|
||||
|
||||
type SettingDefinition struct {
|
||||
Key string
|
||||
EnvName string
|
||||
Label string
|
||||
Type SettingType
|
||||
Editable bool
|
||||
HardLimit bool
|
||||
Minimum int64
|
||||
}
|
||||
|
||||
type SettingRow struct {
|
||||
Definition SettingDefinition
|
||||
Value string
|
||||
Source Source
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DataDir string
|
||||
UploadsDir string
|
||||
DBDir string
|
||||
|
||||
AdminPassword string
|
||||
AdminUsername string
|
||||
AdminEmail string
|
||||
AdminEnabled AdminEnabledMode
|
||||
AdminCookieSecure bool
|
||||
AllowAdminSettingsOverride bool
|
||||
|
||||
GuestUploadsEnabled bool
|
||||
APIEnabled bool
|
||||
ZipDownloadsEnabled bool
|
||||
OneTimeDownloadsEnabled bool
|
||||
RenewOnAccessEnabled bool
|
||||
RenewOnDownloadEnabled bool
|
||||
|
||||
DefaultGuestExpirySeconds int64
|
||||
MaxGuestExpirySeconds int64
|
||||
GlobalMaxFileSizeBytes int64
|
||||
GlobalMaxBoxSizeBytes int64
|
||||
DefaultUserMaxFileSizeBytes int64
|
||||
DefaultUserMaxBoxSizeBytes int64
|
||||
SessionTTLSeconds int64
|
||||
BoxPollIntervalMS int
|
||||
ThumbnailBatchSize int
|
||||
ThumbnailIntervalSeconds int
|
||||
|
||||
sources map[string]Source
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
var Definitions = []SettingDefinition{
|
||||
{Key: SettingDataDir, EnvName: "WARPBOX_DATA_DIR", Label: "Data directory", Type: SettingTypeText, Editable: false, HardLimit: true},
|
||||
{Key: SettingGuestUploadsEnabled, EnvName: "WARPBOX_GUEST_UPLOADS_ENABLED", Label: "Guest uploads enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingAPIEnabled, EnvName: "WARPBOX_API_ENABLED", Label: "API enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingZipDownloadsEnabled, EnvName: "WARPBOX_ZIP_DOWNLOADS_ENABLED", Label: "ZIP downloads enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingOneTimeDownloadsEnabled, EnvName: "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", Label: "One-time downloads enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingRenewOnAccessEnabled, EnvName: "WARPBOX_RENEW_ON_ACCESS_ENABLED", Label: "Renew on access enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingRenewOnDownloadEnabled, EnvName: "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", Label: "Renew on download enabled", Type: SettingTypeBool, Editable: true},
|
||||
{Key: SettingDefaultGuestExpirySecs, EnvName: "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", Label: "Default guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||
{Key: SettingMaxGuestExpirySecs, EnvName: "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", Label: "Max guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||
{Key: SettingGlobalMaxFileSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", Label: "Global max file size bytes", Type: SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0},
|
||||
{Key: SettingGlobalMaxBoxSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", Label: "Global max box size bytes", Type: SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0},
|
||||
{Key: SettingDefaultUserMaxFileBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", Label: "Default user max file size bytes", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||
{Key: SettingDefaultUserMaxBoxBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", Label: "Default user max box size bytes", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||
{Key: SettingSessionTTLSeconds, EnvName: "WARPBOX_SESSION_TTL_SECONDS", Label: "Session TTL seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60},
|
||||
{Key: SettingBoxPollIntervalMS, EnvName: "WARPBOX_BOX_POLL_INTERVAL_MS", Label: "Box poll interval milliseconds", Type: SettingTypeInt, Editable: true, Minimum: 1000},
|
||||
{Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
||||
{Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1},
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
DataDir: "./data",
|
||||
AdminUsername: "admin",
|
||||
AdminEnabled: AdminEnabledAuto,
|
||||
AllowAdminSettingsOverride: true,
|
||||
GuestUploadsEnabled: true,
|
||||
APIEnabled: true,
|
||||
ZipDownloadsEnabled: true,
|
||||
OneTimeDownloadsEnabled: true,
|
||||
DefaultGuestExpirySeconds: 10,
|
||||
MaxGuestExpirySeconds: 48 * 60 * 60,
|
||||
SessionTTLSeconds: 24 * 60 * 60,
|
||||
BoxPollIntervalMS: 5000,
|
||||
ThumbnailBatchSize: 10,
|
||||
ThumbnailIntervalSeconds: 30,
|
||||
sources: make(map[string]Source),
|
||||
values: make(map[string]string),
|
||||
}
|
||||
|
||||
cfg.captureDefaults()
|
||||
|
||||
if err := cfg.applyStringEnv(SettingDataDir, "WARPBOX_DATA_DIR", &cfg.DataDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_PASSWORD", &cfg.AdminPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_USERNAME", &cfg.AdminUsername); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_EMAIL", &cfg.AdminEmail); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw := strings.TrimSpace(os.Getenv("WARPBOX_ADMIN_ENABLED")); raw != "" {
|
||||
mode := AdminEnabledMode(strings.ToLower(raw))
|
||||
if mode != AdminEnabledAuto && mode != AdminEnabledTrue && mode != AdminEnabledFalse {
|
||||
return nil, fmt.Errorf("WARPBOX_ADMIN_ENABLED must be auto, true, or false")
|
||||
}
|
||||
cfg.AdminEnabled = mode
|
||||
}
|
||||
if err := cfg.applyBoolEnv("", "WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE", &cfg.AllowAdminSettingsOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cfg.applyBoolEnv("", "WARPBOX_ADMIN_COOKIE_SECURE", &cfg.AdminCookieSecure); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
envBools := []struct {
|
||||
key string
|
||||
name string
|
||||
target *bool
|
||||
}{
|
||||
{SettingGuestUploadsEnabled, "WARPBOX_GUEST_UPLOADS_ENABLED", &cfg.GuestUploadsEnabled},
|
||||
{SettingAPIEnabled, "WARPBOX_API_ENABLED", &cfg.APIEnabled},
|
||||
{SettingZipDownloadsEnabled, "WARPBOX_ZIP_DOWNLOADS_ENABLED", &cfg.ZipDownloadsEnabled},
|
||||
{SettingOneTimeDownloadsEnabled, "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", &cfg.OneTimeDownloadsEnabled},
|
||||
{SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled},
|
||||
{SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled},
|
||||
}
|
||||
for _, item := range envBools {
|
||||
if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
envInt64s := []struct {
|
||||
key string
|
||||
name string
|
||||
min int64
|
||||
target *int64
|
||||
}{
|
||||
{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
|
||||
{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
|
||||
{SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", 0, &cfg.GlobalMaxFileSizeBytes},
|
||||
{SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", 0, &cfg.GlobalMaxBoxSizeBytes},
|
||||
{SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", 0, &cfg.DefaultUserMaxFileSizeBytes},
|
||||
{SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", 0, &cfg.DefaultUserMaxBoxSizeBytes},
|
||||
{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
|
||||
}
|
||||
for _, item := range envInt64s {
|
||||
if err := cfg.applyInt64Env(item.key, item.name, item.min, item.target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
envInts := []struct {
|
||||
key string
|
||||
name string
|
||||
min int
|
||||
target *int
|
||||
}{
|
||||
{SettingBoxPollIntervalMS, "WARPBOX_BOX_POLL_INTERVAL_MS", 1000, &cfg.BoxPollIntervalMS},
|
||||
{SettingThumbnailBatchSize, "WARPBOX_THUMBNAIL_BATCH_SIZE", 1, &cfg.ThumbnailBatchSize},
|
||||
{SettingThumbnailIntervalSeconds, "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", 1, &cfg.ThumbnailIntervalSeconds},
|
||||
}
|
||||
for _, item := range envInts {
|
||||
if err := cfg.applyIntEnv(item.key, item.name, item.min, item.target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cfg.DataDir = filepath.Clean(cfg.DataDir)
|
||||
if strings.TrimSpace(cfg.DataDir) == "" || cfg.DataDir == "." && strings.TrimSpace(os.Getenv("WARPBOX_DATA_DIR")) == "" {
|
||||
cfg.DataDir = "data"
|
||||
}
|
||||
if cfg.AdminUsername = strings.TrimSpace(cfg.AdminUsername); cfg.AdminUsername == "" {
|
||||
return nil, fmt.Errorf("WARPBOX_ADMIN_USERNAME cannot be empty")
|
||||
}
|
||||
cfg.AdminEmail = strings.TrimSpace(cfg.AdminEmail)
|
||||
cfg.UploadsDir = filepath.Join(cfg.DataDir, "uploads")
|
||||
cfg.DBDir = filepath.Join(cfg.DataDir, "db")
|
||||
cfg.setValue(SettingDataDir, cfg.DataDir, cfg.sourceFor(SettingDataDir))
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) EnsureDirectories() error {
|
||||
for _, path := range []string{cfg.DataDir, cfg.UploadsDir, cfg.DBDir} {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return fmt.Errorf("create %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) ApplyOverrides(overrides map[string]string) error {
|
||||
if !cfg.AllowAdminSettingsOverride {
|
||||
return nil
|
||||
}
|
||||
for key, value := range overrides {
|
||||
if err := cfg.ApplyOverride(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) ApplyOverride(key string, value string) error {
|
||||
def, ok := Definition(key)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown setting %q", key)
|
||||
}
|
||||
if !def.Editable || def.HardLimit {
|
||||
return fmt.Errorf("setting %q cannot be changed from the admin UI", key)
|
||||
}
|
||||
|
||||
switch def.Type {
|
||||
case SettingTypeBool:
|
||||
parsed, err := parseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
cfg.assignBool(key, parsed, SourceDB)
|
||||
case SettingTypeInt64:
|
||||
parsed, err := parseInt64(value, def.Minimum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
cfg.assignInt64(key, parsed, SourceDB)
|
||||
case SettingTypeInt:
|
||||
parsed64, err := parseInt64(value, def.Minimum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
cfg.assignInt(key, int(parsed64), SourceDB)
|
||||
default:
|
||||
return fmt.Errorf("setting %q is not runtime editable", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) SettingRows() []SettingRow {
|
||||
rows := make([]SettingRow, 0, len(Definitions))
|
||||
for _, def := range Definitions {
|
||||
rows = append(rows, SettingRow{
|
||||
Definition: def,
|
||||
Value: cfg.values[def.Key],
|
||||
Source: cfg.sourceFor(def.Key),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (cfg *Config) Source(key string) Source {
|
||||
return cfg.sourceFor(key)
|
||||
}
|
||||
|
||||
func (cfg *Config) AdminLoginEnabled(hasAdminUser bool) bool {
|
||||
switch cfg.AdminEnabled {
|
||||
case AdminEnabledFalse:
|
||||
return false
|
||||
case AdminEnabledTrue:
|
||||
return hasAdminUser
|
||||
default:
|
||||
return hasAdminUser
|
||||
}
|
||||
}
|
||||
|
||||
func Definition(key string) (SettingDefinition, bool) {
|
||||
for _, def := range Definitions {
|
||||
if def.Key == key {
|
||||
return def, true
|
||||
}
|
||||
}
|
||||
return SettingDefinition{}, false
|
||||
}
|
||||
|
||||
func EditableDefinitions() []SettingDefinition {
|
||||
defs := make([]SettingDefinition, 0, len(Definitions))
|
||||
for _, def := range Definitions {
|
||||
if def.Editable && !def.HardLimit {
|
||||
defs = append(defs, def)
|
||||
}
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (cfg *Config) captureDefaults() {
|
||||
cfg.setValue(SettingDataDir, cfg.DataDir, SourceDefault)
|
||||
cfg.setValue(SettingGuestUploadsEnabled, formatBool(cfg.GuestUploadsEnabled), SourceDefault)
|
||||
cfg.setValue(SettingAPIEnabled, formatBool(cfg.APIEnabled), SourceDefault)
|
||||
cfg.setValue(SettingZipDownloadsEnabled, formatBool(cfg.ZipDownloadsEnabled), SourceDefault)
|
||||
cfg.setValue(SettingOneTimeDownloadsEnabled, formatBool(cfg.OneTimeDownloadsEnabled), SourceDefault)
|
||||
cfg.setValue(SettingRenewOnAccessEnabled, formatBool(cfg.RenewOnAccessEnabled), SourceDefault)
|
||||
cfg.setValue(SettingRenewOnDownloadEnabled, formatBool(cfg.RenewOnDownloadEnabled), SourceDefault)
|
||||
cfg.setValue(SettingDefaultGuestExpirySecs, strconv.FormatInt(cfg.DefaultGuestExpirySeconds, 10), SourceDefault)
|
||||
cfg.setValue(SettingMaxGuestExpirySecs, strconv.FormatInt(cfg.MaxGuestExpirySeconds, 10), SourceDefault)
|
||||
cfg.setValue(SettingGlobalMaxFileSizeBytes, strconv.FormatInt(cfg.GlobalMaxFileSizeBytes, 10), SourceDefault)
|
||||
cfg.setValue(SettingGlobalMaxBoxSizeBytes, strconv.FormatInt(cfg.GlobalMaxBoxSizeBytes, 10), SourceDefault)
|
||||
cfg.setValue(SettingDefaultUserMaxFileBytes, strconv.FormatInt(cfg.DefaultUserMaxFileSizeBytes, 10), SourceDefault)
|
||||
cfg.setValue(SettingDefaultUserMaxBoxBytes, strconv.FormatInt(cfg.DefaultUserMaxBoxSizeBytes, 10), SourceDefault)
|
||||
cfg.setValue(SettingSessionTTLSeconds, strconv.FormatInt(cfg.SessionTTLSeconds, 10), SourceDefault)
|
||||
cfg.setValue(SettingBoxPollIntervalMS, strconv.Itoa(cfg.BoxPollIntervalMS), SourceDefault)
|
||||
cfg.setValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize), SourceDefault)
|
||||
cfg.setValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds), SourceDefault)
|
||||
}
|
||||
|
||||
func (cfg *Config) applyStringEnv(key string, name string, target *string) error {
|
||||
raw := os.Getenv(name)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
*target = raw
|
||||
if key != "" {
|
||||
cfg.setValue(key, raw, SourceEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) applyBoolEnv(key string, name string, target *bool) error {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := parseBool(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
*target = parsed
|
||||
if key != "" {
|
||||
cfg.setValue(key, formatBool(parsed), SourceEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) applyInt64Env(key string, name string, min int64, target *int64) error {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := parseInt64(raw, min)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
*target = parsed
|
||||
if key != "" {
|
||||
cfg.setValue(key, strconv.FormatInt(parsed, 10), SourceEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) applyIntEnv(key string, name string, min int, target *int) error {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := parseInt(raw, min)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
*target = parsed
|
||||
if key != "" {
|
||||
cfg.setValue(key, strconv.Itoa(parsed), SourceEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) assignBool(key string, value bool, source Source) {
|
||||
switch key {
|
||||
case SettingGuestUploadsEnabled:
|
||||
cfg.GuestUploadsEnabled = value
|
||||
case SettingAPIEnabled:
|
||||
cfg.APIEnabled = value
|
||||
case SettingZipDownloadsEnabled:
|
||||
cfg.ZipDownloadsEnabled = value
|
||||
case SettingOneTimeDownloadsEnabled:
|
||||
cfg.OneTimeDownloadsEnabled = value
|
||||
case SettingRenewOnAccessEnabled:
|
||||
cfg.RenewOnAccessEnabled = value
|
||||
case SettingRenewOnDownloadEnabled:
|
||||
cfg.RenewOnDownloadEnabled = value
|
||||
}
|
||||
cfg.setValue(key, formatBool(value), source)
|
||||
}
|
||||
|
||||
func (cfg *Config) assignInt64(key string, value int64, source Source) {
|
||||
switch key {
|
||||
case SettingDefaultGuestExpirySecs:
|
||||
cfg.DefaultGuestExpirySeconds = value
|
||||
case SettingMaxGuestExpirySecs:
|
||||
cfg.MaxGuestExpirySeconds = value
|
||||
case SettingDefaultUserMaxFileBytes:
|
||||
cfg.DefaultUserMaxFileSizeBytes = value
|
||||
case SettingDefaultUserMaxBoxBytes:
|
||||
cfg.DefaultUserMaxBoxSizeBytes = value
|
||||
case SettingSessionTTLSeconds:
|
||||
cfg.SessionTTLSeconds = value
|
||||
}
|
||||
cfg.setValue(key, strconv.FormatInt(value, 10), source)
|
||||
}
|
||||
|
||||
func (cfg *Config) assignInt(key string, value int, source Source) {
|
||||
switch key {
|
||||
case SettingBoxPollIntervalMS:
|
||||
cfg.BoxPollIntervalMS = value
|
||||
case SettingThumbnailBatchSize:
|
||||
cfg.ThumbnailBatchSize = value
|
||||
case SettingThumbnailIntervalSeconds:
|
||||
cfg.ThumbnailIntervalSeconds = value
|
||||
}
|
||||
cfg.setValue(key, strconv.Itoa(value), source)
|
||||
}
|
||||
|
||||
func (cfg *Config) setValue(key string, value string, source Source) {
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
cfg.values[key] = value
|
||||
cfg.sources[key] = source
|
||||
}
|
||||
|
||||
func (cfg *Config) sourceFor(key string) Source {
|
||||
source, ok := cfg.sources[key]
|
||||
if !ok {
|
||||
return SourceDefault
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
func parseBool(value string) (bool, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "t", "true", "y", "yes", "on":
|
||||
return true, nil
|
||||
case "0", "f", "false", "n", "no", "off":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("must be a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt64(value string, min int64) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("must be an integer")
|
||||
}
|
||||
if parsed < min {
|
||||
return 0, fmt.Errorf("must be at least %d", min)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseInt(value string, min int) (int, error) {
|
||||
parsed64, err := parseInt64(value, int64(min))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if parsed64 > int64(^uint(0)>>1) {
|
||||
return 0, fmt.Errorf("is too large")
|
||||
}
|
||||
return int(parsed64), nil
|
||||
}
|
||||
|
||||
func formatBool(value bool) string {
|
||||
if value {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
145
lib/config/config_test.go
Normal file
145
lib/config/config_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaults(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.UploadsDir != filepath.Join("data", "uploads") {
|
||||
t.Fatalf("unexpected uploads dir: %s", cfg.UploadsDir)
|
||||
}
|
||||
if cfg.DBDir != filepath.Join("data", "db") {
|
||||
t.Fatalf("unexpected db dir: %s", cfg.DBDir)
|
||||
}
|
||||
if !cfg.GuestUploadsEnabled || !cfg.APIEnabled || !cfg.ZipDownloadsEnabled || !cfg.OneTimeDownloadsEnabled {
|
||||
t.Fatal("expected default guest/API/download toggles to be enabled")
|
||||
}
|
||||
if cfg.AdminUsername != "admin" {
|
||||
t.Fatalf("unexpected admin username: %s", cfg.AdminUsername)
|
||||
}
|
||||
if cfg.AdminPassword != "" {
|
||||
t.Fatal("expected default admin password to be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentOverrides(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("WARPBOX_DATA_DIR", "/tmp/warpbox-test")
|
||||
t.Setenv("WARPBOX_GUEST_UPLOADS_ENABLED", "false")
|
||||
t.Setenv("WARPBOX_API_ENABLED", "false")
|
||||
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", "100")
|
||||
t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000")
|
||||
t.Setenv("WARPBOX_ADMIN_USERNAME", "root")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.UploadsDir != filepath.Join("/tmp/warpbox-test", "uploads") {
|
||||
t.Fatalf("unexpected uploads dir: %s", cfg.UploadsDir)
|
||||
}
|
||||
if cfg.GuestUploadsEnabled || cfg.APIEnabled {
|
||||
t.Fatal("expected boolean environment overrides to be applied")
|
||||
}
|
||||
if cfg.GlobalMaxFileSizeBytes != 100 {
|
||||
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
|
||||
}
|
||||
if cfg.BoxPollIntervalMS != 2000 {
|
||||
t.Fatalf("unexpected poll interval: %d", cfg.BoxPollIntervalMS)
|
||||
}
|
||||
if cfg.AdminUsername != "root" {
|
||||
t.Fatalf("unexpected admin username: %s", cfg.AdminUsername)
|
||||
}
|
||||
if cfg.Source(SettingAPIEnabled) != SourceEnv {
|
||||
t.Fatalf("expected API setting source to be env, got %s", cfg.Source(SettingAPIEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidEnvironmentValues(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("WARPBOX_SESSION_TTL_SECONDS", "1")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected invalid session ttl to fail")
|
||||
}
|
||||
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("WARPBOX_GUEST_UPLOADS_ENABLED", "maybe")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected invalid boolean to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsOverridePrecedence(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("WARPBOX_API_ENABLED", "true")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if err := cfg.ApplyOverrides(map[string]string{SettingAPIEnabled: "false"}); err != nil {
|
||||
t.Fatalf("ApplyOverrides returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.APIEnabled {
|
||||
t.Fatal("expected DB override to beat environment value")
|
||||
}
|
||||
if cfg.Source(SettingAPIEnabled) != SourceDB {
|
||||
t.Fatalf("expected DB source, got %s", cfg.Source(SettingAPIEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsOverrideValidation(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if err := cfg.ApplyOverride(SettingDefaultGuestExpirySecs, "-1"); err == nil {
|
||||
t.Fatal("expected negative expiry override to fail")
|
||||
}
|
||||
if err := cfg.ApplyOverride(SettingGlobalMaxFileSizeBytes, "1"); err == nil {
|
||||
t.Fatal("expected hard limit override to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func clearConfigEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, name := range []string{
|
||||
"WARPBOX_DATA_DIR",
|
||||
"WARPBOX_ADMIN_PASSWORD",
|
||||
"WARPBOX_ADMIN_USERNAME",
|
||||
"WARPBOX_ADMIN_EMAIL",
|
||||
"WARPBOX_ADMIN_ENABLED",
|
||||
"WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE",
|
||||
"WARPBOX_ADMIN_COOKIE_SECURE",
|
||||
"WARPBOX_GUEST_UPLOADS_ENABLED",
|
||||
"WARPBOX_API_ENABLED",
|
||||
"WARPBOX_ZIP_DOWNLOADS_ENABLED",
|
||||
"WARPBOX_ONE_TIME_DOWNLOADS_ENABLED",
|
||||
"WARPBOX_RENEW_ON_ACCESS_ENABLED",
|
||||
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
|
||||
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
|
||||
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
|
||||
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
|
||||
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
|
||||
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
|
||||
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
|
||||
"WARPBOX_SESSION_TTL_SECONDS",
|
||||
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
||||
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
||||
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
||||
} {
|
||||
t.Setenv(name, "")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user