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

@@ -220,6 +220,29 @@ func TestAdminUploadBypassesMaxUploadSize(t *testing.T) {
}
}
func TestUnlimitedAnonymousUploadPolicyUsesNegativeOne(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
policy, err := app.settingsService.UploadPolicy()
if err != nil {
t.Fatalf("UploadPolicy returned error: %v", err)
}
policy.AnonymousMaxUploadMB = -1
policy.AnonymousDailyUploadMB = -1
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
}
request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", int(app.uploadService.MaxUploadSize())+1))
request.Header.Set("Accept", "application/json")
response := httptest.NewRecorder()
app.Upload(response, request)
if response.Code != http.StatusCreated {
t.Fatalf("unlimited anonymous upload status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestAnonymousUploadDisabled(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()

View File

@@ -359,15 +359,15 @@ func (a *App) AdminSettingsPost(w http.ResponseWriter, r *http.Request) {
if value := r.FormValue("user_storage_backend"); value != "" {
settings.UserStorageBackend = value
}
if settings.AnonymousMaxUploadMB, err = services.ParseMegabytesValue(r.FormValue("anonymous_max_upload_mb")); err != nil {
if settings.AnonymousMaxUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_max_upload_mb")); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if settings.AnonymousDailyUploadMB, err = services.ParseMegabytesValue(r.FormValue("anonymous_daily_upload_mb")); err != nil {
if settings.AnonymousDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_daily_upload_mb")); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if settings.UserDailyUploadMB, err = services.ParseMegabytesValue(r.FormValue("user_daily_upload_mb")); err != nil {
if settings.UserDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("user_daily_upload_mb")); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -839,7 +839,7 @@ func optionalMB(value string) *float64 {
if value == "" {
return nil
}
parsed, err := services.ParseMegabytesValue(value)
parsed, err := services.ParseMegabytesLimitValue(value)
if err != nil {
return nil
}

View File

@@ -66,7 +66,9 @@ func (a *App) homeUploadPolicyLabels(settings services.UploadPolicySettings, use
}
policy := a.settingsService.EffectivePolicyForUser(settings, user)
maxUpload := a.uploadService.MaxUploadSizeLabel()
if policy.MaxUploadMB > 0 {
if policy.MaxUploadMB < 0 {
maxUpload = "unlimited"
} else if policy.MaxUploadMB > 0 {
maxUpload = services.FormatMegabytesLabel(policy.MaxUploadMB)
}
quota := "unlimited"

View File

@@ -39,12 +39,14 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
return
}
if !isAdminUpload {
r.Body = http.MaxBytesReader(w, r.Body, uploadParseLimit(effectivePolicy, loggedIn, a.uploadService.MaxUploadSize()))
}
parseLimit := uploadParseLimit(effectivePolicy, loggedIn, a.uploadService.MaxUploadSize())
if !isAdminUpload && parseLimit > 0 {
r.Body = http.MaxBytesReader(w, r.Body, parseLimit)
}
if isAdminUpload {
parseLimit = 32 << 20
} else if parseLimit <= 0 {
parseLimit = 32 << 20
}
if err := r.ParseMultipartForm(parseLimit); err != nil {
helpers.WriteJSONError(w, http.StatusBadRequest, "upload form could not be read")
@@ -84,7 +86,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
ObfuscateMetadata: r.FormValue("obfuscate_metadata") == "on",
OwnerID: ownerID,
CollectionID: collectionID,
SkipSizeLimit: isAdminUpload,
SkipSizeLimit: isAdminUpload || effectivePolicy.MaxUploadMB < 0,
CreatorIP: uploadClientIP(r),
StorageBackendID: effectivePolicy.StorageBackendID,
})
@@ -131,7 +133,7 @@ func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bo
if err != nil {
return http.StatusInternalServerError, "upload usage could not be checked"
}
if usage.UploadedBytes+totalBytes > services.MegabytesToBytes(policy.DailyUploadMB) {
if policy.DailyUploadMB > 0 && usage.UploadedBytes+totalBytes > services.MegabytesToBytes(policy.DailyUploadMB) {
return http.StatusTooManyRequests, "anonymous daily upload limit reached"
}
if usage.UploadedBoxes+1 > policy.DailyBoxes {
@@ -154,7 +156,7 @@ func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bo
if err != nil {
return http.StatusInternalServerError, "upload usage could not be checked"
}
if usage.UploadedBytes+totalBytes > services.MegabytesToBytes(policy.DailyUploadMB) {
if policy.DailyUploadMB > 0 && usage.UploadedBytes+totalBytes > services.MegabytesToBytes(policy.DailyUploadMB) {
return http.StatusTooManyRequests, "daily upload limit reached"
}
if usage.UploadedBoxes+1 > policy.DailyBoxes {
@@ -214,6 +216,9 @@ func (a *App) checkStorageBackendCapacity(backendID string, settings services.Up
}
func uploadParseLimit(policy services.EffectiveUploadPolicy, loggedIn bool, fallback int64) int64 {
if policy.MaxUploadMB < 0 {
return -1
}
if loggedIn && policy.MaxUploadMB <= 0 {
return fallback * 8
}