feat(config): allow -1 to represent unlimited upload limits

Introduce support for configuring unlimited upload limits by allowing -1
as a valid value for anonymous and user upload MB limits.

Changes include:
- Added `envMegabytesLimitFloat` and helper functions to parse and validate limits where -1 is allowed.
- Updated validation logic to accept -1 for `AnonymousMaxUploadMB`, `AnonymousDailyUploadMB`, and `UserDailyUploadMB`.
- Added a test case to verify unlimited upload policy behavior.
This commit is contained in:
2026-05-31 14:01:38 +03:00
parent 61b7c283a4
commit f1c67c455b
12 changed files with 194 additions and 34 deletions

View File

@@ -856,11 +856,11 @@ func VerifyPasswordHash(encoded, password string) bool {
}
func validateUserPolicy(policy UserPolicy) error {
if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 {
return fmt.Errorf("max upload override cannot be negative")
if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 && *policy.MaxUploadMB != -1 {
return fmt.Errorf("max upload override must be positive or -1 for unlimited")
}
if policy.DailyUploadMB != nil && *policy.DailyUploadMB <= 0 {
return fmt.Errorf("daily upload override must be positive")
if policy.DailyUploadMB != nil && ((*policy.DailyUploadMB < 0 && *policy.DailyUploadMB != -1) || *policy.DailyUploadMB == 0) {
return fmt.Errorf("daily upload override must be positive or -1 for unlimited")
}
if policy.StorageQuotaMB != nil && *policy.StorageQuotaMB < 0 {
return fmt.Errorf("storage quota override cannot be negative")

View File

@@ -205,6 +205,26 @@ func TestAPITokenScopedToOwnerAndDisabledUser(t *testing.T) {
}
}
func TestUserPolicyAllowsNegativeOneForUnlimitedUploadLimits(t *testing.T) {
auth := newTestAuthService(t)
user, err := auth.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
if err != nil {
t.Fatalf("CreateBootstrapUser returned error: %v", err)
}
unlimited := -1.0
if err := auth.SetUserPolicy(user.ID, UserPolicy{MaxUploadMB: &unlimited, DailyUploadMB: &unlimited}); err != nil {
t.Fatalf("SetUserPolicy rejected -1 unlimited upload limits: %v", err)
}
updated, err := auth.UserByID(user.ID)
if err != nil {
t.Fatalf("UserByID returned error: %v", err)
}
if updated.Policy.MaxUploadMB == nil || *updated.Policy.MaxUploadMB != -1 || updated.Policy.DailyUploadMB == nil || *updated.Policy.DailyUploadMB != -1 {
t.Fatalf("unlimited policy was not persisted: %+v", updated.Policy)
}
}
func newTestAuthService(t *testing.T) *AuthService {
t.Helper()
root := t.TempDir()

View File

@@ -170,13 +170,13 @@ func (s *SettingsService) UploadPolicy() (UploadPolicySettings, error) {
}
func (s *SettingsService) withDefaultGaps(settings UploadPolicySettings) UploadPolicySettings {
if settings.AnonymousMaxUploadMB <= 0 {
if settings.AnonymousMaxUploadMB == 0 {
settings.AnonymousMaxUploadMB = s.defaults.AnonymousMaxUploadMB
}
if settings.AnonymousDailyUploadMB <= 0 {
if settings.AnonymousDailyUploadMB == 0 {
settings.AnonymousDailyUploadMB = s.defaults.AnonymousDailyUploadMB
}
if settings.UserDailyUploadMB <= 0 {
if settings.UserDailyUploadMB == 0 {
settings.UserDailyUploadMB = s.defaults.UserDailyUploadMB
}
if settings.DefaultUserStorageMB <= 0 {
@@ -370,14 +370,14 @@ func (s *SettingsService) UsageForIP(ip string, now time.Time) (UsageRecord, err
}
func (s *SettingsService) validate(settings UploadPolicySettings) error {
if settings.AnonymousMaxUploadMB <= 0 {
return fmt.Errorf("anonymous max upload must be positive")
if settings.AnonymousMaxUploadMB < 0 && settings.AnonymousMaxUploadMB != -1 || settings.AnonymousMaxUploadMB == 0 {
return fmt.Errorf("anonymous max upload must be positive or -1 for unlimited")
}
if settings.AnonymousDailyUploadMB <= 0 {
return fmt.Errorf("anonymous daily upload must be positive")
if settings.AnonymousDailyUploadMB < 0 && settings.AnonymousDailyUploadMB != -1 || settings.AnonymousDailyUploadMB == 0 {
return fmt.Errorf("anonymous daily upload must be positive or -1 for unlimited")
}
if settings.UserDailyUploadMB <= 0 {
return fmt.Errorf("user daily upload must be positive")
if settings.UserDailyUploadMB < 0 && settings.UserDailyUploadMB != -1 || settings.UserDailyUploadMB == 0 {
return fmt.Errorf("user daily upload must be positive or -1 for unlimited")
}
if settings.DefaultUserStorageMB <= 0 {
return fmt.Errorf("default user storage must be positive")
@@ -422,6 +422,32 @@ func ParseMegabytesValue(value string) (float64, error) {
return parsed, nil
}
func ParseMegabytesLimitValue(value string) (float64, error) {
parsed, err := parseMegabytesNumber(value)
if err != nil {
return 0, err
}
if parsed == -1 {
return -1, nil
}
if parsed <= 0 {
return 0, fmt.Errorf("megabyte value must be positive or -1 for unlimited")
}
return parsed, nil
}
func parseMegabytesNumber(value string) (float64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("megabyte value is required")
}
value = strings.TrimSuffix(value, "MB")
value = strings.TrimSuffix(value, "Mb")
value = strings.TrimSuffix(value, "mb")
value = strings.TrimSpace(value)
return strconv.ParseFloat(value, 64)
}
func MegabytesToBytes(value float64) int64 {
return int64(value * 1024 * 1024)
}
@@ -437,6 +463,9 @@ func FormatMegabytesFromBytes(value int64) string {
}
func FormatMegabytesLabel(value float64) string {
if value < 0 {
return "unlimited"
}
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
}

View File

@@ -117,6 +117,30 @@ func TestSettingsRejectInvalidMegabytes(t *testing.T) {
}
}
func TestUploadPolicyAllowsNegativeOneForUnlimitedUploadLimits(t *testing.T) {
settings := newTestSettingsService(t)
policy, err := settings.UploadPolicy()
if err != nil {
t.Fatalf("UploadPolicy returned error: %v", err)
}
policy.AnonymousMaxUploadMB = -1
policy.AnonymousDailyUploadMB = -1
policy.UserDailyUploadMB = -1
if err := settings.UpdateUploadPolicy(policy); err != nil {
t.Fatalf("UpdateUploadPolicy rejected -1 unlimited upload limits: %v", err)
}
next, err := settings.UploadPolicy()
if err != nil {
t.Fatalf("UploadPolicy returned error: %v", err)
}
if next.AnonymousMaxUploadMB != -1 || next.AnonymousDailyUploadMB != -1 || next.UserDailyUploadMB != -1 {
t.Fatalf("unlimited upload limits were not persisted: %+v", next)
}
if got := FormatMegabytesLabel(-1); got != "unlimited" {
t.Fatalf("FormatMegabytesLabel(-1) = %q, want unlimited", got)
}
}
func TestDailyUsageAndCleanup(t *testing.T) {
settings := newTestSettingsService(t)
now := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)