package config import ( "fmt" "math" "os" "path/filepath" "strconv" "strings" ) func Load() (*Config, error) { cfg := &Config{ DataDir: "./data", AdminUsername: "admin", AdminEnabled: AdminEnabledAuto, AllowAdminSettingsOverride: true, GuestUploadsEnabled: true, APIEnabled: true, ZipDownloadsEnabled: true, OneTimeDownloadsEnabled: true, OneTimeDownloadExpirySeconds: 7 * 24 * 60 * 60, OneTimeDownloadRetryOnFailure: false, 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), } // Config precedence: defaults -> env -> overrides. // Overrides are applied after Load by the server once the metadata store opens. 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}, {SettingOneTimeDownloadRetryFail, "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", &cfg.OneTimeDownloadRetryOnFailure}, {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}, {SettingOneTimeDownloadExpirySecs, "WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS", 0, &cfg.OneTimeDownloadExpirySeconds}, {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 } } sizeEnvVars := []struct { key string mbName string bytesName string target *int64 }{ {SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", &cfg.GlobalMaxFileSizeBytes}, {SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", &cfg.GlobalMaxBoxSizeBytes}, {SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", &cfg.DefaultUserMaxFileSizeBytes}, {SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", &cfg.DefaultUserMaxBoxSizeBytes}, } for _, item := range sizeEnvVars { if err := cfg.applyMegabytesOrBytesEnv(item.key, item.mbName, item.bytesName, 0, 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) 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(SettingOneTimeDownloadExpirySecs, strconv.FormatInt(cfg.OneTimeDownloadExpirySeconds, 10), SourceDefault) cfg.setValue(SettingOneTimeDownloadRetryFail, formatBool(cfg.OneTimeDownloadRetryOnFailure), 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) applyMegabytesOrBytesEnv(key string, mbName string, bytesName string, min int64, target *int64) error { if rawBytes := strings.TrimSpace(os.Getenv(bytesName)); rawBytes != "" { parsed, err := parseInt64(rawBytes, min) if err != nil { return fmt.Errorf("%s: %w", bytesName, err) } *target = parsed cfg.setValue(key, strconv.FormatInt(parsed, 10), SourceEnv) return nil } rawMB := strings.TrimSpace(os.Getenv(mbName)) if rawMB == "" { return nil } parsedMB, err := parseInt64(rawMB, min) if err != nil { return fmt.Errorf("%s: %w", mbName, err) } if parsedMB > math.MaxInt64/(1024*1024) { return fmt.Errorf("%s: is too large", mbName) } parsedBytes := parsedMB * 1024 * 1024 *target = parsedBytes cfg.setValue(key, strconv.FormatInt(parsedBytes, 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 }