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

@@ -72,9 +72,9 @@ func Load() (Config, error) {
MaxUploadSize: envMegabytes("WARPBOX_MAX_UPLOAD_SIZE_MB", 2048), // 2 GiB default. MaxUploadSize: envMegabytes("WARPBOX_MAX_UPLOAD_SIZE_MB", 2048), // 2 GiB default.
DefaultSettings: SettingsDefaults{ DefaultSettings: SettingsDefaults{
AnonymousUploadsEnabled: envBool("WARPBOX_ANONYMOUS_UPLOADS_ENABLED", true), AnonymousUploadsEnabled: envBool("WARPBOX_ANONYMOUS_UPLOADS_ENABLED", true),
AnonymousMaxUploadMB: envMegabytesFloat("WARPBOX_ANONYMOUS_MAX_UPLOAD_MB", 512), AnonymousMaxUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_MAX_UPLOAD_MB", 512),
AnonymousDailyUploadMB: envMegabytesFloat("WARPBOX_ANONYMOUS_DAILY_UPLOAD_MB", 2048), AnonymousDailyUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_DAILY_UPLOAD_MB", 2048),
UserDailyUploadMB: envMegabytesFloat("WARPBOX_USER_DAILY_UPLOAD_MB", 8192), UserDailyUploadMB: envMegabytesLimitFloat("WARPBOX_USER_DAILY_UPLOAD_MB", 8192),
DefaultUserStorageMB: envMegabytesFloat("WARPBOX_DEFAULT_USER_STORAGE_MB", 51200), DefaultUserStorageMB: envMegabytesFloat("WARPBOX_DEFAULT_USER_STORAGE_MB", 51200),
UsageRetentionDays: envInt("WARPBOX_USAGE_RETENTION_DAYS", 30), UsageRetentionDays: envInt("WARPBOX_USAGE_RETENTION_DAYS", 30),
LocalStorageMaxGB: envGigabytesFloat("WARPBOX_LOCAL_STORAGE_MAX_GB", 100), LocalStorageMaxGB: envGigabytesFloat("WARPBOX_LOCAL_STORAGE_MAX_GB", 100),
@@ -97,9 +97,9 @@ func Load() (Config, error) {
if cfg.MaxUploadSize <= 0 { if cfg.MaxUploadSize <= 0 {
return Config{}, fmt.Errorf("WARPBOX_MAX_UPLOAD_SIZE_MB must be positive") return Config{}, fmt.Errorf("WARPBOX_MAX_UPLOAD_SIZE_MB must be positive")
} }
if cfg.DefaultSettings.AnonymousMaxUploadMB <= 0 || if !validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousMaxUploadMB) ||
cfg.DefaultSettings.AnonymousDailyUploadMB <= 0 || !validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousDailyUploadMB) ||
cfg.DefaultSettings.UserDailyUploadMB <= 0 || !validUnlimitedMegabyteLimit(cfg.DefaultSettings.UserDailyUploadMB) ||
cfg.DefaultSettings.DefaultUserStorageMB <= 0 || cfg.DefaultSettings.DefaultUserStorageMB <= 0 ||
cfg.DefaultSettings.UsageRetentionDays <= 0 || cfg.DefaultSettings.UsageRetentionDays <= 0 ||
cfg.DefaultSettings.LocalStorageMaxGB <= 0 || cfg.DefaultSettings.LocalStorageMaxGB <= 0 ||
@@ -111,7 +111,7 @@ func Load() (Config, error) {
cfg.DefaultSettings.UserActiveBoxes <= 0 || cfg.DefaultSettings.UserActiveBoxes <= 0 ||
cfg.DefaultSettings.ShortWindowRequests <= 0 || cfg.DefaultSettings.ShortWindowRequests <= 0 ||
cfg.DefaultSettings.ShortWindowSeconds <= 0 { cfg.DefaultSettings.ShortWindowSeconds <= 0 {
return Config{}, fmt.Errorf("upload policy settings must be positive") return Config{}, fmt.Errorf("upload policy settings must be positive, with -1 allowed for upload MB limits")
} }
return cfg, nil return cfg, nil
@@ -203,6 +203,18 @@ func envMegabytesFloat(key string, fallback float64) float64 {
return parsed return parsed
} }
func envMegabytesLimitFloat(key string, fallback float64) float64 {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := parseMegabytesLimitFloat(value)
if err != nil {
return fallback
}
return parsed
}
func envGigabytesFloat(key string, fallback float64) float64 { func envGigabytesFloat(key string, fallback float64) float64 {
value := strings.TrimSpace(os.Getenv(key)) value := strings.TrimSpace(os.Getenv(key))
if value == "" { if value == "" {
@@ -246,6 +258,35 @@ func parseMegabytesFloat(value string) (float64, error) {
return sizeMB, nil return sizeMB, nil
} }
func parseMegabytesLimitFloat(value string) (float64, error) {
sizeMB, err := parseMegabytesFloatAllowNegativeOne(value)
if err != nil {
return 0, err
}
if !validUnlimitedMegabyteLimit(sizeMB) {
return 0, fmt.Errorf("megabyte value must be positive or -1 for unlimited")
}
return sizeMB, nil
}
func parseMegabytesFloatAllowNegativeOne(value string) (float64, error) {
normalized := strings.TrimSpace(value)
normalized = strings.TrimSuffix(normalized, "MB")
normalized = strings.TrimSuffix(normalized, "Mb")
normalized = strings.TrimSuffix(normalized, "mb")
normalized = strings.TrimSpace(normalized)
sizeMB, err := strconv.ParseFloat(normalized, 64)
if err != nil {
return 0, fmt.Errorf("invalid megabyte value %q: %w", value, err)
}
return sizeMB, nil
}
func validUnlimitedMegabyteLimit(value float64) bool {
return value > 0 || value == -1
}
func megabytesToBytes(sizeMB float64) int64 { func megabytesToBytes(sizeMB float64) int64 {
return int64(math.Round(sizeMB * 1024 * 1024)) return int64(math.Round(sizeMB * 1024 * 1024))
} }

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) { func TestAnonymousUploadDisabled(t *testing.T) {
app, cleanup := newTestApp(t) app, cleanup := newTestApp(t)
defer cleanup() 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 != "" { if value := r.FormValue("user_storage_backend"); value != "" {
settings.UserStorageBackend = 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) http.Error(w, err.Error(), http.StatusBadRequest)
return 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) http.Error(w, err.Error(), http.StatusBadRequest)
return 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) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@@ -839,7 +839,7 @@ func optionalMB(value string) *float64 {
if value == "" { if value == "" {
return nil return nil
} }
parsed, err := services.ParseMegabytesValue(value) parsed, err := services.ParseMegabytesLimitValue(value)
if err != nil { if err != nil {
return nil return nil
} }

View File

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

View File

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

View File

@@ -856,11 +856,11 @@ func VerifyPasswordHash(encoded, password string) bool {
} }
func validateUserPolicy(policy UserPolicy) error { func validateUserPolicy(policy UserPolicy) error {
if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 { if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 && *policy.MaxUploadMB != -1 {
return fmt.Errorf("max upload override cannot be negative") return fmt.Errorf("max upload override must be positive or -1 for unlimited")
} }
if policy.DailyUploadMB != nil && *policy.DailyUploadMB <= 0 { if policy.DailyUploadMB != nil && ((*policy.DailyUploadMB < 0 && *policy.DailyUploadMB != -1) || *policy.DailyUploadMB == 0) {
return fmt.Errorf("daily upload override must be positive") return fmt.Errorf("daily upload override must be positive or -1 for unlimited")
} }
if policy.StorageQuotaMB != nil && *policy.StorageQuotaMB < 0 { if policy.StorageQuotaMB != nil && *policy.StorageQuotaMB < 0 {
return fmt.Errorf("storage quota override cannot be negative") 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 { func newTestAuthService(t *testing.T) *AuthService {
t.Helper() t.Helper()
root := t.TempDir() root := t.TempDir()

View File

@@ -170,13 +170,13 @@ func (s *SettingsService) UploadPolicy() (UploadPolicySettings, error) {
} }
func (s *SettingsService) withDefaultGaps(settings UploadPolicySettings) UploadPolicySettings { func (s *SettingsService) withDefaultGaps(settings UploadPolicySettings) UploadPolicySettings {
if settings.AnonymousMaxUploadMB <= 0 { if settings.AnonymousMaxUploadMB == 0 {
settings.AnonymousMaxUploadMB = s.defaults.AnonymousMaxUploadMB settings.AnonymousMaxUploadMB = s.defaults.AnonymousMaxUploadMB
} }
if settings.AnonymousDailyUploadMB <= 0 { if settings.AnonymousDailyUploadMB == 0 {
settings.AnonymousDailyUploadMB = s.defaults.AnonymousDailyUploadMB settings.AnonymousDailyUploadMB = s.defaults.AnonymousDailyUploadMB
} }
if settings.UserDailyUploadMB <= 0 { if settings.UserDailyUploadMB == 0 {
settings.UserDailyUploadMB = s.defaults.UserDailyUploadMB settings.UserDailyUploadMB = s.defaults.UserDailyUploadMB
} }
if settings.DefaultUserStorageMB <= 0 { 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 { func (s *SettingsService) validate(settings UploadPolicySettings) error {
if settings.AnonymousMaxUploadMB <= 0 { if settings.AnonymousMaxUploadMB < 0 && settings.AnonymousMaxUploadMB != -1 || settings.AnonymousMaxUploadMB == 0 {
return fmt.Errorf("anonymous max upload must be positive") return fmt.Errorf("anonymous max upload must be positive or -1 for unlimited")
} }
if settings.AnonymousDailyUploadMB <= 0 { if settings.AnonymousDailyUploadMB < 0 && settings.AnonymousDailyUploadMB != -1 || settings.AnonymousDailyUploadMB == 0 {
return fmt.Errorf("anonymous daily upload must be positive") return fmt.Errorf("anonymous daily upload must be positive or -1 for unlimited")
} }
if settings.UserDailyUploadMB <= 0 { if settings.UserDailyUploadMB < 0 && settings.UserDailyUploadMB != -1 || settings.UserDailyUploadMB == 0 {
return fmt.Errorf("user daily upload must be positive") return fmt.Errorf("user daily upload must be positive or -1 for unlimited")
} }
if settings.DefaultUserStorageMB <= 0 { if settings.DefaultUserStorageMB <= 0 {
return fmt.Errorf("default user storage must be positive") return fmt.Errorf("default user storage must be positive")
@@ -422,6 +422,32 @@ func ParseMegabytesValue(value string) (float64, error) {
return parsed, nil 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 { func MegabytesToBytes(value float64) int64 {
return int64(value * 1024 * 1024) return int64(value * 1024 * 1024)
} }
@@ -437,6 +463,9 @@ func FormatMegabytesFromBytes(value int64) string {
} }
func FormatMegabytesLabel(value float64) string { func FormatMegabytesLabel(value float64) string {
if value < 0 {
return "unlimited"
}
return strconv.FormatFloat(value, 'f', -1, 64) + " MB" 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) { func TestDailyUsageAndCleanup(t *testing.T) {
settings := newTestSettingsService(t) settings := newTestSettingsService(t)
now := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)

View File

@@ -22,6 +22,7 @@
} }
.metric-card { .metric-card {
min-width: 0;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
background: rgba(24, 24, 27, 0.78); background: rgba(24, 24, 27, 0.78);
@@ -37,11 +38,26 @@
.metric-card strong { .metric-card strong {
display: block; display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 0.4rem; margin-top: 0.4rem;
color: var(--foreground); color: var(--foreground);
font-size: 1.35rem; font-size: 1.35rem;
} }
.metric-card span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-edit-metrics {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-table-card { .admin-table-card {
margin-top: 1rem; margin-top: 1rem;
} }

View File

@@ -34,7 +34,7 @@
<div class="table-header"> <div class="table-header">
<div> <div>
<h2>Upload policy</h2> <h2>Upload policy</h2>
<p>Admin users bypass all upload caps. Values are in megabytes.</p> <p>Admin users bypass all upload caps. Values are in megabytes; use -1 for unlimited upload size or daily upload caps.</p>
</div> </div>
</div> </div>

View File

@@ -38,7 +38,7 @@
</div> </div>
{{end}} {{end}}
<div class="metric-grid"> <div class="metric-grid user-edit-metrics">
<article class="metric-card"><span>Storage used</span><strong>{{.Data.UserEdit.StorageUsed}}</strong></article> <article class="metric-card"><span>Storage used</span><strong>{{.Data.UserEdit.StorageUsed}}</strong></article>
<article class="metric-card"><span>Uploaded today</span><strong>{{.Data.UserEdit.DailyUsed}}</strong></article> <article class="metric-card"><span>Uploaded today</span><strong>{{.Data.UserEdit.DailyUsed}}</strong></article>
<article class="metric-card"><span>Effective quota</span><strong>{{.Data.UserEdit.EffectiveStorage}}</strong></article> <article class="metric-card"><span>Effective quota</span><strong>{{.Data.UserEdit.EffectiveStorage}}</strong></article>
@@ -50,7 +50,7 @@
<div class="table-header"> <div class="table-header">
<div> <div>
<h2>Identity and limits</h2> <h2>Identity and limits</h2>
<p>Blank limit fields inherit the global user defaults. Storage quota set to 0 means unlimited.</p> <p>Blank limit fields inherit the global user defaults. Use -1 for unlimited upload size or daily upload caps. Storage quota set to 0 means unlimited.</p>
</div> </div>
</div> </div>
<form class="settings-form" action="/admin/users/{{.Data.UserEdit.ID}}/edit" method="post"> <form class="settings-form" action="/admin/users/{{.Data.UserEdit.ID}}/edit" method="post">