refactor(storage): standardize size limits to use GB units

This commit is contained in:
2026-05-01 02:14:05 +03:00
parent d0aa86205f
commit 1cf38d126d
17 changed files with 255 additions and 114 deletions

View File

@@ -35,7 +35,7 @@ func TestEnvironmentOverrides(t *testing.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_GLOBAL_MAX_FILE_SIZE_GB", "0.5")
t.Setenv("WARPBOX_BOX_POLL_INTERVAL_MS", "2000")
t.Setenv("WARPBOX_ADMIN_USERNAME", "root")
t.Setenv("WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "true")
@@ -51,7 +51,7 @@ func TestEnvironmentOverrides(t *testing.T) {
if cfg.GuestUploadsEnabled || cfg.APIEnabled {
t.Fatal("expected boolean environment overrides to be applied")
}
if cfg.GlobalMaxFileSizeBytes != 100 {
if cfg.GlobalMaxFileSizeBytes != 512*1024*1024 {
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
}
if cfg.BoxPollIntervalMS != 2000 {
@@ -70,25 +70,25 @@ func TestEnvironmentOverrides(t *testing.T) {
func TestMegabyteSizeEnvironmentOverrides(t *testing.T) {
clearConfigEnv(t)
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "2048")
t.Setenv("WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "4096")
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_GB", "2")
t.Setenv("WARPBOX_GLOBAL_MAX_BOX_SIZE_GB", "4")
cfg, err := Load()
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
if cfg.GlobalMaxFileSizeBytes != 2048*1024*1024 {
if cfg.GlobalMaxFileSizeBytes != 2*1024*1024*1024 {
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
}
if cfg.GlobalMaxBoxSizeBytes != 4096*1024*1024 {
if cfg.GlobalMaxBoxSizeBytes != 4*1024*1024*1024 {
t.Fatalf("unexpected global max box size: %d", cfg.GlobalMaxBoxSizeBytes)
}
}
func TestByteSizeEnvironmentOverridesTakePrecedence(t *testing.T) {
func TestGBEnvironmentOverridesTakePrecedenceOverLegacySizeEnvNames(t *testing.T) {
clearConfigEnv(t)
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "2048")
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_GB", "2")
t.Setenv("WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", "100")
cfg, err := Load()
@@ -96,7 +96,7 @@ func TestByteSizeEnvironmentOverridesTakePrecedence(t *testing.T) {
t.Fatalf("Load returned error: %v", err)
}
if cfg.GlobalMaxFileSizeBytes != 100 {
if cfg.GlobalMaxFileSizeBytes != 2*1024*1024*1024 {
t.Fatalf("unexpected global max file size: %d", cfg.GlobalMaxFileSizeBytes)
}
}
@@ -145,10 +145,10 @@ func TestSettingsOverrideValidation(t *testing.T) {
if err := cfg.ApplyOverride(SettingDefaultGuestExpirySecs, "-1"); err == nil {
t.Fatal("expected negative expiry override to fail")
}
if err := cfg.ApplyOverride(SettingGlobalMaxFileSizeBytes, "1"); err != nil {
if err := cfg.ApplyOverride(SettingGlobalMaxFileSizeBytes, "0.5"); err != nil {
t.Fatalf("expected global max file size override to succeed, got %v", err)
}
if cfg.GlobalMaxFileSizeBytes != 1 {
if cfg.GlobalMaxFileSizeBytes != 512*1024*1024 {
t.Fatalf("expected global max file size override to apply, got %d", cfg.GlobalMaxFileSizeBytes)
}
if err := cfg.ApplyOverride(SettingDataDir, "/tmp/elsewhere"); err == nil {
@@ -175,12 +175,16 @@ func clearConfigEnv(t *testing.T) {
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_GB",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_MB",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_GB",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_MB",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
"WARPBOX_SESSION_TTL_SECONDS",

View File

@@ -12,10 +12,10 @@ var Definitions = []SettingDefinition{
{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: true, Minimum: 0},
{Key: SettingGlobalMaxBoxSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", Label: "Global max box size bytes", Type: SettingTypeInt64, Editable: 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: SettingGlobalMaxFileSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_FILE_SIZE_GB", Label: "Global max file size GB", Type: SettingTypeSizeGB, Editable: true, Minimum: 0},
{Key: SettingGlobalMaxBoxSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_BOX_SIZE_GB", Label: "Global max box size GB", Type: SettingTypeSizeGB, Editable: true, Minimum: 0},
{Key: SettingDefaultUserMaxFileBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB", Label: "Default user max file size GB", Type: SettingTypeSizeGB, Editable: true, Minimum: 0},
{Key: SettingDefaultUserMaxBoxBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB", Label: "Default user max box size GB", Type: SettingTypeSizeGB, 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},
@@ -50,6 +50,7 @@ func (cfg *Config) AdminLoginEnabled(hasAdminUser bool) bool {
}
func Definition(key string) (SettingDefinition, bool) {
key = NormalizeLegacySettingKey(key)
for _, def := range Definitions {
if def.Key == key {
return def, true
@@ -58,6 +59,35 @@ func Definition(key string) (SettingDefinition, bool) {
return SettingDefinition{}, false
}
func NormalizeLegacySettingKey(key string) string {
switch key {
case "global_max_file_size_bytes":
return SettingGlobalMaxFileSizeBytes
case "global_max_box_size_bytes":
return SettingGlobalMaxBoxSizeBytes
case "default_user_max_file_size_bytes":
return SettingDefaultUserMaxFileBytes
case "default_user_max_box_size_bytes":
return SettingDefaultUserMaxBoxBytes
default:
return key
}
}
func NormalizeOverrideInput(key string, value string) (string, string, error) {
normalizedKey := NormalizeLegacySettingKey(key)
switch key {
case "global_max_file_size_bytes", "global_max_box_size_bytes", "default_user_max_file_size_bytes", "default_user_max_box_size_bytes":
parsed, err := parseInt64(value, 0)
if err != nil {
return normalizedKey, "", err
}
return normalizedKey, formatGigabytesFromBytes(parsed), nil
default:
return normalizedKey, value, nil
}
}
func EditableDefinitions() []SettingDefinition {
defs := make([]SettingDefinition, 0, len(Definitions))
for _, def := range Definitions {

View File

@@ -2,7 +2,6 @@ package config
import (
"fmt"
"math"
"os"
"path/filepath"
"strconv"
@@ -99,17 +98,18 @@ func Load() (*Config, error) {
}
sizeEnvVars := []struct {
key string
gbName 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},
{SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_GB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", &cfg.GlobalMaxFileSizeBytes},
{SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_GB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", &cfg.GlobalMaxBoxSizeBytes},
{SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", &cfg.DefaultUserMaxFileSizeBytes},
{SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB", "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 {
if err := cfg.applySizeEnv(item.key, item.gbName, item.mbName, item.bytesName, 0, item.target); err != nil {
return nil, err
}
}
@@ -164,10 +164,10 @@ func (cfg *Config) captureDefaults() {
cfg.captureDefaultValue(SettingRenewOnDownloadEnabled, formatBool(cfg.RenewOnDownloadEnabled))
cfg.captureDefaultValue(SettingDefaultGuestExpirySecs, strconv.FormatInt(cfg.DefaultGuestExpirySeconds, 10))
cfg.captureDefaultValue(SettingMaxGuestExpirySecs, strconv.FormatInt(cfg.MaxGuestExpirySeconds, 10))
cfg.captureDefaultValue(SettingGlobalMaxFileSizeBytes, strconv.FormatInt(cfg.GlobalMaxFileSizeBytes, 10))
cfg.captureDefaultValue(SettingGlobalMaxBoxSizeBytes, strconv.FormatInt(cfg.GlobalMaxBoxSizeBytes, 10))
cfg.captureDefaultValue(SettingDefaultUserMaxFileBytes, strconv.FormatInt(cfg.DefaultUserMaxFileSizeBytes, 10))
cfg.captureDefaultValue(SettingDefaultUserMaxBoxBytes, strconv.FormatInt(cfg.DefaultUserMaxBoxSizeBytes, 10))
cfg.captureDefaultValue(SettingGlobalMaxFileSizeBytes, formatGigabytesFromBytes(cfg.GlobalMaxFileSizeBytes))
cfg.captureDefaultValue(SettingGlobalMaxBoxSizeBytes, formatGigabytesFromBytes(cfg.GlobalMaxBoxSizeBytes))
cfg.captureDefaultValue(SettingDefaultUserMaxFileBytes, formatGigabytesFromBytes(cfg.DefaultUserMaxFileSizeBytes))
cfg.captureDefaultValue(SettingDefaultUserMaxBoxBytes, formatGigabytesFromBytes(cfg.DefaultUserMaxBoxSizeBytes))
cfg.captureDefaultValue(SettingSessionTTLSeconds, strconv.FormatInt(cfg.SessionTTLSeconds, 10))
cfg.captureDefaultValue(SettingBoxPollIntervalMS, strconv.Itoa(cfg.BoxPollIntervalMS))
cfg.captureDefaultValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize))
@@ -225,14 +225,23 @@ func (cfg *Config) applyInt64Env(key string, name string, min int64, target *int
return nil
}
func (cfg *Config) applyMegabytesOrBytesEnv(key string, mbName string, bytesName string, min int64, target *int64) error {
func (cfg *Config) applySizeEnv(key string, gbName string, mbName string, bytesName string, min int64, target *int64) error {
if rawGB := strings.TrimSpace(os.Getenv(gbName)); rawGB != "" {
parsed, err := parseGigabytes(rawGB, float64(min))
if err != nil {
return fmt.Errorf("%s: %w", gbName, err)
}
*target = parsed
cfg.setValue(key, formatGigabytesFromBytes(parsed), SourceEnv)
return nil
}
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)
cfg.setValue(key, formatGigabytesFromBytes(parsed), SourceEnv)
return nil
}
@@ -244,12 +253,9 @@ func (cfg *Config) applyMegabytesOrBytesEnv(key string, mbName string, bytesName
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
parsedBytes := parsedMB * 1000 * 1000
*target = parsedBytes
cfg.setValue(key, strconv.FormatInt(parsedBytes, 10), SourceEnv)
cfg.setValue(key, formatGigabytesFromBytes(parsedBytes), SourceEnv)
return nil
}

View File

@@ -27,10 +27,10 @@ const (
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"
SettingGlobalMaxFileSizeBytes = "global_max_file_size_gb"
SettingGlobalMaxBoxSizeBytes = "global_max_box_size_gb"
SettingDefaultUserMaxFileBytes = "default_user_max_file_size_gb"
SettingDefaultUserMaxBoxBytes = "default_user_max_box_size_gb"
SettingSessionTTLSeconds = "session_ttl_seconds"
SettingBoxPollIntervalMS = "box_poll_interval_ms"
SettingThumbnailBatchSize = "thumbnail_batch_size"
@@ -41,10 +41,11 @@ const (
type SettingType string
const (
SettingTypeBool SettingType = "bool"
SettingTypeInt64 SettingType = "int64"
SettingTypeInt SettingType = "int"
SettingTypeText SettingType = "text"
SettingTypeBool SettingType = "bool"
SettingTypeInt64 SettingType = "int64"
SettingTypeInt SettingType = "int"
SettingTypeText SettingType = "text"
SettingTypeSizeGB SettingType = "size_gb"
)
type SettingDefinition struct {

View File

@@ -32,7 +32,15 @@ func ReadAdminSettingsOverrides(path string) (map[string]string, error) {
if payload.Overrides == nil {
return map[string]string{}, nil
}
return payload.Overrides, nil
normalized := make(map[string]string, len(payload.Overrides))
for key, value := range payload.Overrides {
nextKey, nextValue, err := NormalizeOverrideInput(key, value)
if err != nil {
return nil, err
}
normalized[nextKey] = nextValue
}
return normalized, nil
}
func WriteAdminSettingsOverrides(path string, overrides map[string]string) error {

View File

@@ -39,6 +39,12 @@ func (cfg *Config) ApplyOverride(key string, value string) error {
return fmt.Errorf("%s: %w", key, err)
}
cfg.assignInt64(key, parsed, SourceDB)
case SettingTypeSizeGB:
parsed, err := parseGigabytes(value, float64(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 {
@@ -87,6 +93,10 @@ func (cfg *Config) assignInt64(key string, value int64, source Source) {
case SettingSessionTTLSeconds:
cfg.SessionTTLSeconds = value
}
if key == SettingGlobalMaxFileSizeBytes || key == SettingGlobalMaxBoxSizeBytes || key == SettingDefaultUserMaxFileBytes || key == SettingDefaultUserMaxBoxBytes {
cfg.setValue(key, formatGigabytesFromBytes(value), source)
return
}
cfg.setValue(key, strconv.FormatInt(value, 10), source)
}

View File

@@ -2,6 +2,7 @@ package config
import (
"fmt"
"math"
"strconv"
"strings"
)
@@ -39,6 +40,46 @@ func parseInt(value string, min int) (int, error) {
return int(parsed64), nil
}
const bytesPerGigabyte = 1024 * 1024 * 1024
func parseGigabytes(value string, min float64) (int64, error) {
raw := strings.TrimSpace(value)
lower := strings.ToLower(raw)
if strings.HasSuffix(lower, "gb") {
raw = strings.TrimSpace(raw[:len(raw)-2])
}
parsed, err := strconv.ParseFloat(raw, 64)
if err != nil {
return 0, fmt.Errorf("must be a number of GB")
}
if parsed < min {
return 0, fmt.Errorf("must be at least %s", trimTrailingZeros(min))
}
bytes := parsed * bytesPerGigabyte
if bytes > math.MaxInt64 {
return 0, fmt.Errorf("is too large")
}
return int64(math.Round(bytes)), nil
}
func formatGigabytesFromBytes(bytes int64) string {
if bytes <= 0 {
return "0"
}
value := float64(bytes) / bytesPerGigabyte
return trimTrailingZeros(value)
}
func trimTrailingZeros(value float64) string {
text := strconv.FormatFloat(value, 'f', 3, 64)
text = strings.TrimRight(text, "0")
text = strings.TrimRight(text, ".")
if text == "" {
return "0"
}
return text
}
func formatBool(value bool) string {
if value {
return "true"

View File

@@ -84,8 +84,19 @@ func (app *App) handleAdminSettingsSave(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid save payload"})
return
}
currentOverrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load current settings overrides"})
return
}
if currentOverrides == nil {
currentOverrides = map[string]string{}
}
for key, value := range request.Values {
currentOverrides[key] = value
}
rows, warnings, err := app.applySettingsOverrideSet(request.Values)
rows, warnings, err := app.applySettingsOverrideSet(currentOverrides)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -168,30 +179,24 @@ func (app *App) handleAdminSettingsReset(ctx *gin.Context) {
var request adminSettingsResetRequest
_ = ctx.ShouldBindJSON(&request)
defs := config.EditableDefinitions()
overrideSet := make(map[string]string, len(defs))
overrideSet, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load settings overrides"})
return
}
if overrideSet == nil {
overrideSet = map[string]string{}
}
targetKeys := map[string]bool{}
for _, key := range request.Keys {
targetKeys[key] = true
targetKeys[config.NormalizeLegacySettingKey(key)] = true
}
if len(targetKeys) == 0 {
for _, def := range defs {
overrideSet[def.Key] = app.config.DefaultValue(def.Key)
}
overrideSet = map[string]string{}
} else {
currentOverrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load settings overrides"})
return
}
for key, value := range currentOverrides {
overrideSet[key] = value
}
for _, def := range defs {
if targetKeys[def.Key] {
overrideSet[def.Key] = app.config.DefaultValue(def.Key)
}
for key := range targetKeys {
delete(overrideSet, key)
}
}
@@ -203,7 +208,7 @@ func (app *App) handleAdminSettingsReset(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"ok": true,
"message": "Editable settings reset to application defaults",
"message": "Selected overrides cleared; environment and defaults now apply",
"warnings": warnings,
"rows": rows,
})
@@ -231,7 +236,12 @@ func (app *App) applySettingsOverrideSet(values map[string]string) ([]adminSetti
sort.Strings(keys)
for _, key := range keys {
value := strings.TrimSpace(values[key])
normalizedKey, normalizedValue, err := config.NormalizeOverrideInput(key, strings.TrimSpace(values[key]))
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", key, err)
}
key = normalizedKey
value := normalizedValue
def, ok := editable[key]
if !ok {
if _, found := config.Definition(key); found {
@@ -447,10 +457,10 @@ func settingsDescription(key string) string {
config.SettingRenewOnDownloadEnabled: "Extend retention when file or ZIP downloads happen.",
config.SettingDefaultGuestExpirySecs: "Default retention presented to guest uploads.",
config.SettingMaxGuestExpirySecs: "Maximum retention guests may request.",
config.SettingGlobalMaxFileSizeBytes: "Global single-file upload ceiling applied to future requests across the whole app.",
config.SettingGlobalMaxBoxSizeBytes: "Global total box size ceiling applied to future requests across the whole app.",
config.SettingDefaultUserMaxFileBytes: "Default per-user file size ceiling used by future account-aware flows.",
config.SettingDefaultUserMaxBoxBytes: "Default per-user box size ceiling used by future account-aware flows.",
config.SettingGlobalMaxFileSizeBytes: "Global single-file upload ceiling in GB applied to future requests across the whole app. Decimal values allowed.",
config.SettingGlobalMaxBoxSizeBytes: "Global total box size ceiling in GB applied to future requests across the whole app. Decimal values allowed.",
config.SettingDefaultUserMaxFileBytes: "Default per-user file size ceiling in GB used by future account-aware flows. Decimal values allowed.",
config.SettingDefaultUserMaxBoxBytes: "Default per-user box size ceiling in GB used by future account-aware flows. Decimal values allowed.",
config.SettingSessionTTLSeconds: "Lifetime for authenticated browser sessions, including admin session cookies.",
config.SettingBoxPollIntervalMS: "Browser polling cadence for box status refreshes.",
config.SettingThumbnailBatchSize: "How many thumbnail jobs the worker handles per batch.",

View File

@@ -82,7 +82,7 @@ func TestAdminSettingsExportIncludesCurrentValues(t *testing.T) {
func TestAdminSettingsSavePersistsEditableOverrides(t *testing.T) {
app, router := setupAdminSettingsTest(t)
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"api_enabled":"true","box_poll_interval_ms":"6000"}}`))
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"api_enabled":"true","box_poll_interval_ms":"6000","global_max_file_size_gb":"0.5"}}`))
request.Header.Set("Content-Type", "application/json")
request.AddCookie(authCookie(app))
response := httptest.NewRecorder()
@@ -97,6 +97,9 @@ func TestAdminSettingsSavePersistsEditableOverrides(t *testing.T) {
if app.config.BoxPollIntervalMS != 6000 {
t.Fatalf("expected poll interval override, got %d", app.config.BoxPollIntervalMS)
}
if app.config.GlobalMaxFileSizeBytes != 512*1024*1024 {
t.Fatalf("expected size override in bytes, got %d", app.config.GlobalMaxFileSizeBytes)
}
overrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
if err != nil {
@@ -105,6 +108,12 @@ func TestAdminSettingsSavePersistsEditableOverrides(t *testing.T) {
if overrides[config.SettingAPIEnabled] != "true" {
t.Fatalf("expected persisted API override, got %#v", overrides)
}
if _, exists := overrides[config.SettingBoxPollIntervalMS]; !exists {
t.Fatalf("expected changed poll interval override to be persisted, got %#v", overrides)
}
if _, exists := overrides[config.SettingSessionTTLSeconds]; exists {
t.Fatalf("expected untouched setting to stay out of overrides, got %#v", overrides)
}
}
func TestAdminSettingsSaveRejectsLockedSetting(t *testing.T) {
@@ -160,8 +169,8 @@ func TestAdminSettingsResetUsesBuiltInDefaults(t *testing.T) {
if response.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String())
}
if !app.config.APIEnabled {
t.Fatal("expected reset to built-in defaults to restore APIEnabled=true")
if app.config.APIEnabled {
t.Fatal("expected reset to respect environment and restore APIEnabled=false")
}
}
@@ -240,12 +249,16 @@ func clearAdminSettingsEnv(t *testing.T) {
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_GB",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_MB",
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_GB",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_MB",
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB",
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB",
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
"WARPBOX_SESSION_TTL_SECONDS",