feat(policy): support unlimited values in user policies and box expiry
- Update user policy and user update handlers to accept -1 as an unlimited value for MaxDays, DailyBoxes, ActiveBoxes, and ShortWindowRequests. - Introduce `optionalIntAllowUnlimited` helper and update `optionalMBAllowZero` to support -1. - Use `boxExpiryLabel` helper across admin, dashboard, and download handlers to properly format expiration dates, supporting boxes that never expire.
This commit is contained in:
@@ -1004,10 +1004,10 @@ func (a *App) AdminUpdateUserPolicy(w http.ResponseWriter, r *http.Request) {
|
|||||||
MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")),
|
MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")),
|
||||||
DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")),
|
DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")),
|
||||||
StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")),
|
StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")),
|
||||||
MaxDays: optionalInt(r.FormValue("max_days")),
|
MaxDays: optionalIntAllowUnlimited(r.FormValue("max_days")),
|
||||||
DailyBoxes: optionalInt(r.FormValue("daily_boxes")),
|
DailyBoxes: optionalIntAllowUnlimited(r.FormValue("daily_boxes")),
|
||||||
ActiveBoxes: optionalInt(r.FormValue("active_boxes")),
|
ActiveBoxes: optionalIntAllowUnlimited(r.FormValue("active_boxes")),
|
||||||
ShortWindowRequests: optionalInt(r.FormValue("short_window_requests")),
|
ShortWindowRequests: optionalIntAllowUnlimited(r.FormValue("short_window_requests")),
|
||||||
}
|
}
|
||||||
if backendID := r.FormValue("storage_backend_id"); backendID != "" {
|
if backendID := r.FormValue("storage_backend_id"); backendID != "" {
|
||||||
if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
|
if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
|
||||||
@@ -1036,10 +1036,10 @@ func (a *App) AdminUpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")),
|
MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")),
|
||||||
DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")),
|
DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")),
|
||||||
StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")),
|
StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")),
|
||||||
MaxDays: optionalInt(r.FormValue("max_days")),
|
MaxDays: optionalIntAllowUnlimited(r.FormValue("max_days")),
|
||||||
DailyBoxes: optionalInt(r.FormValue("daily_boxes")),
|
DailyBoxes: optionalIntAllowUnlimited(r.FormValue("daily_boxes")),
|
||||||
ActiveBoxes: optionalInt(r.FormValue("active_boxes")),
|
ActiveBoxes: optionalIntAllowUnlimited(r.FormValue("active_boxes")),
|
||||||
ShortWindowRequests: optionalInt(r.FormValue("short_window_requests")),
|
ShortWindowRequests: optionalIntAllowUnlimited(r.FormValue("short_window_requests")),
|
||||||
}
|
}
|
||||||
if backendID := r.FormValue("storage_backend_id"); backendID != "" {
|
if backendID := r.FormValue("storage_backend_id"); backendID != "" {
|
||||||
if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
|
if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
|
||||||
@@ -1206,7 +1206,7 @@ func (a *App) adminBoxes(limit int) ([]adminBoxView, error) {
|
|||||||
ID: box.ID,
|
ID: box.ID,
|
||||||
Owner: owner,
|
Owner: owner,
|
||||||
CreatedAt: box.CreatedAt.Format("Jan 2 15:04"),
|
CreatedAt: box.CreatedAt.Format("Jan 2 15:04"),
|
||||||
ExpiresAt: box.ExpiresAt.Format("Jan 2 15:04"),
|
ExpiresAt: boxExpiryLabel(box.ExpiresAt, "Jan 2 15:04"),
|
||||||
FileCount: box.FileCount,
|
FileCount: box.FileCount,
|
||||||
TotalSizeLabel: box.TotalSizeLabel,
|
TotalSizeLabel: box.TotalSizeLabel,
|
||||||
DownloadCount: box.DownloadCount,
|
DownloadCount: box.DownloadCount,
|
||||||
@@ -1296,7 +1296,8 @@ func optionalMBAllowZero(value string) *float64 {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
parsed, err := strconv.ParseFloat(value, 64)
|
parsed, err := strconv.ParseFloat(value, 64)
|
||||||
if err != nil || parsed < 0 {
|
// 0 and -1 both mean unlimited; reject other negatives.
|
||||||
|
if err != nil || (parsed < 0 && parsed != -1) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &parsed
|
return &parsed
|
||||||
@@ -1313,6 +1314,18 @@ func optionalInt(value string) *int {
|
|||||||
return &parsed
|
return &parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optionalIntAllowUnlimited is like optionalInt but also accepts -1 (unlimited).
|
||||||
|
func optionalIntAllowUnlimited(value string) *int {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := strconv.Atoi(value)
|
||||||
|
if err != nil || (parsed <= 0 && parsed != -1) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &parsed
|
||||||
|
}
|
||||||
|
|
||||||
func formatMB(value float64) string {
|
func formatMB(value float64) string {
|
||||||
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
|
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (a *App) Dashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
FileCount: len(row.Box.Files),
|
FileCount: len(row.Box.Files),
|
||||||
Size: row.TotalSizeLabel,
|
Size: row.TotalSizeLabel,
|
||||||
CreatedAt: row.Box.CreatedAt.Format("Jan 2 15:04"),
|
CreatedAt: row.Box.CreatedAt.Format("Jan 2 15:04"),
|
||||||
ExpiresAt: row.Box.ExpiresAt.Format("Jan 2 15:04"),
|
ExpiresAt: boxExpiryLabel(row.Box.ExpiresAt, "Jan 2 15:04"),
|
||||||
URL: "/d/" + row.Box.ID,
|
URL: "/d/" + row.Box.ID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresLabel := box.ExpiresAt.Format("Jan 2, 2006 15:04 MST")
|
expiresLabel := boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST")
|
||||||
title := "Shared files on Warpbox"
|
title := "Shared files on Warpbox"
|
||||||
description := fmt.Sprintf("%d file%s shared via Warpbox · expires %s", len(box.Files), plural(len(box.Files)), expiresLabel)
|
description := fmt.Sprintf("%d file%s shared via Warpbox · expires %s", len(box.Files), plural(len(box.Files)), expiresLabel)
|
||||||
if locked && box.Obfuscate {
|
if locked && box.Obfuscate {
|
||||||
@@ -337,6 +337,21 @@ func unlockCookieName(boxID string) string {
|
|||||||
return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID)
|
return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// neverExpires reports whether a box's expiry is far enough out to be treated as
|
||||||
|
// "forever" (set via the unlimited / -1 expiry option).
|
||||||
|
func neverExpires(t time.Time) bool {
|
||||||
|
return time.Until(t) > 50*365*24*time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// boxExpiryLabel formats a box's expiry with the given layout, rendering
|
||||||
|
// "forever" boxes as "Never" instead of a meaningless far-future date.
|
||||||
|
func boxExpiryLabel(t time.Time, layout string) string {
|
||||||
|
if neverExpires(t) {
|
||||||
|
return "Never"
|
||||||
|
}
|
||||||
|
return t.Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
func absoluteURL(r *http.Request, path string) string {
|
func absoluteURL(r *http.Request, path string) string {
|
||||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||||
return path
|
return path
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (a *App) managePageData(box services.Box, token string) managePageData {
|
|||||||
Token: token,
|
Token: token,
|
||||||
FileCount: len(box.Files),
|
FileCount: len(box.Files),
|
||||||
TotalSize: helpers.FormatBytes(totalSize),
|
TotalSize: helpers.FormatBytes(totalSize),
|
||||||
ExpiresLabel: box.ExpiresAt.Format("Jan 2, 2006 15:04 MST"),
|
ExpiresLabel: boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST"),
|
||||||
DownloadCount: box.DownloadCount,
|
DownloadCount: box.DownloadCount,
|
||||||
MaxDownloads: box.MaxDownloads,
|
MaxDownloads: box.MaxDownloads,
|
||||||
Protected: a.uploadService.IsProtected(box),
|
Protected: a.uploadService.IsProtected(box),
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ func (a *App) homeExpiryOptions(settings services.UploadPolicySettings, user ser
|
|||||||
unlimited = true
|
unlimited = true
|
||||||
case loggedIn:
|
case loggedIn:
|
||||||
maxDays = a.settingsService.EffectivePolicyForUser(settings, user).MaxDays
|
maxDays = a.settingsService.EffectivePolicyForUser(settings, user).MaxDays
|
||||||
|
// A negative per-user MaxDays override means unlimited retention.
|
||||||
|
if maxDays < 0 {
|
||||||
|
unlimited = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return buildExpiryOptions(maxDays, unlimited)
|
return buildExpiryOptions(maxDays, unlimited)
|
||||||
}
|
}
|
||||||
@@ -103,6 +107,10 @@ func buildExpiryOptions(maxDays int, unlimited bool) ([]expiryOption, int) {
|
|||||||
if len(options) == 0 {
|
if len(options) == 0 {
|
||||||
options = append(options, expiryOption{Minutes: capMinutes, Label: expiryLabel(capMinutes)})
|
options = append(options, expiryOption{Minutes: capMinutes, Label: expiryLabel(capMinutes)})
|
||||||
}
|
}
|
||||||
|
// Unlimited uploaders can pick "never expires" (sentinel -1) after the ladder.
|
||||||
|
if unlimited {
|
||||||
|
options = append(options, expiryOption{Minutes: -1, Label: "Unlimited (never expires)"})
|
||||||
|
}
|
||||||
|
|
||||||
// Default to 24h when available, otherwise the smallest option offered.
|
// Default to 24h when available, otherwise the smallest option offered.
|
||||||
defaultMinutes := options[0].Minutes
|
defaultMinutes := options[0].Minutes
|
||||||
@@ -154,5 +162,9 @@ func (a *App) homeUploadPolicyLabels(settings services.UploadPolicySettings, use
|
|||||||
if policy.StorageQuotaSet {
|
if policy.StorageQuotaSet {
|
||||||
quota = services.FormatMegabytesLabel(policy.StorageQuotaMB)
|
quota = services.FormatMegabytesLabel(policy.StorageQuotaMB)
|
||||||
}
|
}
|
||||||
return maxUpload, "Daily cap: " + services.FormatMegabytesLabel(policy.DailyUploadMB) + " · Storage quota: " + quota + " · " + strconv.Itoa(policy.MaxDays) + " day max."
|
expiryLimit := strconv.Itoa(policy.MaxDays) + " day max."
|
||||||
|
if policy.MaxDays < 0 {
|
||||||
|
expiryLimit = "no expiry limit."
|
||||||
|
}
|
||||||
|
return maxUpload, "Daily cap: " + services.FormatMegabytesLabel(policy.DailyUploadMB) + " · Storage quota: " + quota + " · " + expiryLimit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
effectivePolicy := a.effectiveUploadPolicy(settings, user, loggedIn)
|
effectivePolicy := a.effectiveUploadPolicy(settings, user, loggedIn)
|
||||||
rateKey := uploadRateKey(r, user, loggedIn)
|
rateKey := uploadRateKey(r, user, loggedIn)
|
||||||
if !isAdminUpload && !a.rateLimiter.Allow("upload:"+rateKey, effectivePolicy.ShortRequests, effectivePolicy.ShortWindow, time.Now().UTC()) {
|
if !isAdminUpload && effectivePolicy.ShortRequests > 0 && !a.rateLimiter.Allow("upload:"+rateKey, effectivePolicy.ShortRequests, effectivePolicy.ShortWindow, time.Now().UTC()) {
|
||||||
a.logger.Warn("upload rate limited", "source", "user-upload", "severity", "warn", "code", 4290, "ip", uploadClientIP(r), "user_id", user.ID)
|
a.logger.Warn("upload rate limited", "source", "user-upload", "severity", "warn", "code", 4290, "ip", uploadClientIP(r), "user_id", user.ID)
|
||||||
helpers.WriteJSONError(w, http.StatusTooManyRequests, "too many upload requests, please slow down")
|
helpers.WriteJSONError(w, http.StatusTooManyRequests, "too many upload requests, please slow down")
|
||||||
return
|
return
|
||||||
@@ -77,17 +77,33 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Unlimited expiry: admins, or users whose effective MaxDays is negative.
|
||||||
|
unlimitedExpiry := isAdminUpload || effectivePolicy.MaxDays < 0
|
||||||
|
|
||||||
maxDays := parseInt(r.FormValue("max_days"))
|
maxDays := parseInt(r.FormValue("max_days"))
|
||||||
if maxDays <= 0 {
|
if maxDays <= 0 {
|
||||||
maxDays = min(7, effectivePolicy.MaxDays)
|
maxDays = 7
|
||||||
|
if effectivePolicy.MaxDays > 0 && effectivePolicy.MaxDays < maxDays {
|
||||||
|
maxDays = effectivePolicy.MaxDays
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !isAdminUpload && maxDays > effectivePolicy.MaxDays {
|
if !unlimitedExpiry && maxDays > effectivePolicy.MaxDays {
|
||||||
a.logger.Warn("upload rejected expiration days", "source", "user-upload", "severity", "warn", "code", 4131, "ip", uploadClientIP(r), "user_id", user.ID, "requested_days", maxDays, "max_days", effectivePolicy.MaxDays)
|
a.logger.Warn("upload rejected expiration days", "source", "user-upload", "severity", "warn", "code", 4131, "ip", uploadClientIP(r), "user_id", user.ID, "requested_days", maxDays, "max_days", effectivePolicy.MaxDays)
|
||||||
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))
|
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresMinutes := parseInt(r.FormValue("expires_minutes"))
|
expiresMinutes := parseInt(r.FormValue("expires_minutes"))
|
||||||
if expiresMinutes > 0 && !isAdminUpload && expiresMinutes > effectivePolicy.MaxDays*24*60 {
|
// A negative expires_minutes (or max_days) is the "never expires" request.
|
||||||
|
// Only honour it for unlimited uploaders; otherwise it's an invalid value.
|
||||||
|
if expiresMinutes < 0 || parseInt(r.FormValue("max_days")) < 0 {
|
||||||
|
if !unlimitedExpiry {
|
||||||
|
a.logger.Warn("upload rejected unlimited expiration", "source", "user-upload", "severity", "warn", "code", 4133, "ip", uploadClientIP(r), "user_id", user.ID)
|
||||||
|
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiresMinutes = -1
|
||||||
|
} else if expiresMinutes > 0 && !unlimitedExpiry && expiresMinutes > effectivePolicy.MaxDays*24*60 {
|
||||||
a.logger.Warn("upload rejected expiration minutes", "source", "user-upload", "severity", "warn", "code", 4132, "ip", uploadClientIP(r), "user_id", user.ID, "requested_minutes", expiresMinutes, "max_days", effectivePolicy.MaxDays)
|
a.logger.Warn("upload rejected expiration minutes", "source", "user-upload", "severity", "warn", "code", 4132, "ip", uploadClientIP(r), "user_id", user.ID, "requested_minutes", expiresMinutes, "max_days", effectivePolicy.MaxDays)
|
||||||
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))
|
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))
|
||||||
return
|
return
|
||||||
@@ -213,14 +229,14 @@ func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bo
|
|||||||
if policy.DailyUploadMB > 0 && 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 policy.DailyBoxes > 0 && usage.UploadedBoxes+1 > policy.DailyBoxes {
|
||||||
return http.StatusTooManyRequests, "anonymous daily box limit reached"
|
return http.StatusTooManyRequests, "anonymous daily box limit reached"
|
||||||
}
|
}
|
||||||
activeBoxes, err := a.uploadService.ActiveBoxCountForIP(uploadClientIP(r))
|
activeBoxes, err := a.uploadService.ActiveBoxCountForIP(uploadClientIP(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, "active box limit could not be checked"
|
return http.StatusInternalServerError, "active box limit could not be checked"
|
||||||
}
|
}
|
||||||
if activeBoxes+1 > policy.ActiveBoxes {
|
if policy.ActiveBoxes > 0 && activeBoxes+1 > policy.ActiveBoxes {
|
||||||
return http.StatusTooManyRequests, "anonymous active box limit reached"
|
return http.StatusTooManyRequests, "anonymous active box limit reached"
|
||||||
}
|
}
|
||||||
if status, message := a.checkStorageBackendCapacity(policy.StorageBackendID, settings, totalBytes); message != "" {
|
if status, message := a.checkStorageBackendCapacity(policy.StorageBackendID, settings, totalBytes); message != "" {
|
||||||
@@ -236,14 +252,14 @@ func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bo
|
|||||||
if policy.DailyUploadMB > 0 && 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 policy.DailyBoxes > 0 && usage.UploadedBoxes+1 > policy.DailyBoxes {
|
||||||
return http.StatusTooManyRequests, "daily box limit reached"
|
return http.StatusTooManyRequests, "daily box limit reached"
|
||||||
}
|
}
|
||||||
activeBoxes, err := a.uploadService.ActiveBoxCountForUser(user.ID)
|
activeBoxes, err := a.uploadService.ActiveBoxCountForUser(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, "active box limit could not be checked"
|
return http.StatusInternalServerError, "active box limit could not be checked"
|
||||||
}
|
}
|
||||||
if activeBoxes+1 > policy.ActiveBoxes {
|
if policy.ActiveBoxes > 0 && activeBoxes+1 > policy.ActiveBoxes {
|
||||||
return http.StatusTooManyRequests, "active box limit reached"
|
return http.StatusTooManyRequests, "active box limit reached"
|
||||||
}
|
}
|
||||||
activeStorage, err := a.uploadService.UserActiveStorageUsed(user.ID)
|
activeStorage, err := a.uploadService.UserActiveStorageUsed(user.ID)
|
||||||
|
|||||||
@@ -862,20 +862,20 @@ func validateUserPolicy(policy UserPolicy) error {
|
|||||||
if policy.DailyUploadMB != nil && ((*policy.DailyUploadMB < 0 && *policy.DailyUploadMB != -1) || *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 or -1 for unlimited")
|
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 && *policy.StorageQuotaMB != -1 {
|
||||||
return fmt.Errorf("storage quota override cannot be negative")
|
return fmt.Errorf("storage quota override must be 0/positive or -1 for unlimited")
|
||||||
}
|
}
|
||||||
if policy.MaxDays != nil && *policy.MaxDays <= 0 {
|
if policy.MaxDays != nil && *policy.MaxDays <= 0 && *policy.MaxDays != -1 {
|
||||||
return fmt.Errorf("expiration override must be positive")
|
return fmt.Errorf("expiration override must be positive or -1 for unlimited")
|
||||||
}
|
}
|
||||||
if policy.DailyBoxes != nil && *policy.DailyBoxes <= 0 {
|
if policy.DailyBoxes != nil && *policy.DailyBoxes <= 0 && *policy.DailyBoxes != -1 {
|
||||||
return fmt.Errorf("daily box override must be positive")
|
return fmt.Errorf("daily box override must be positive or -1 for unlimited")
|
||||||
}
|
}
|
||||||
if policy.ActiveBoxes != nil && *policy.ActiveBoxes <= 0 {
|
if policy.ActiveBoxes != nil && *policy.ActiveBoxes <= 0 && *policy.ActiveBoxes != -1 {
|
||||||
return fmt.Errorf("active box override must be positive")
|
return fmt.Errorf("active box override must be positive or -1 for unlimited")
|
||||||
}
|
}
|
||||||
if policy.ShortWindowRequests != nil && *policy.ShortWindowRequests <= 0 {
|
if policy.ShortWindowRequests != nil && *policy.ShortWindowRequests <= 0 && *policy.ShortWindowRequests != -1 {
|
||||||
return fmt.Errorf("short-window request override must be positive")
|
return fmt.Errorf("short-window request override must be positive or -1 for unlimited")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,14 +198,21 @@ func (s *UploadService) CreateBox(files []*multipart.FileHeader, opts UploadOpti
|
|||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return UploadResult{}, fmt.Errorf("no files were uploaded")
|
return UploadResult{}, fmt.Errorf("no files were uploaded")
|
||||||
}
|
}
|
||||||
if opts.MaxDays <= 0 {
|
|
||||||
opts.MaxDays = 7
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
expiresAt := now.Add(time.Duration(opts.MaxDays) * 24 * time.Hour)
|
var expiresAt time.Time
|
||||||
if opts.ExpiresInMinutes > 0 {
|
switch {
|
||||||
|
case opts.ExpiresInMinutes < 0 || opts.MaxDays < 0:
|
||||||
|
// "Forever" — a date far enough out that the box effectively never
|
||||||
|
// expires. No schema change; CanDownload/cleanup keep working as-is.
|
||||||
|
expiresAt = now.AddDate(100, 0, 0)
|
||||||
|
case opts.ExpiresInMinutes > 0:
|
||||||
expiresAt = now.Add(time.Duration(opts.ExpiresInMinutes) * time.Minute)
|
expiresAt = now.Add(time.Duration(opts.ExpiresInMinutes) * time.Minute)
|
||||||
|
default:
|
||||||
|
days := opts.MaxDays
|
||||||
|
if days <= 0 {
|
||||||
|
days = 7
|
||||||
|
}
|
||||||
|
expiresAt = now.Add(time.Duration(days) * 24 * time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
box := Box{
|
box := Box{
|
||||||
|
|||||||
BIN
backend/static/WarpBoxLogo.png
Normal file
BIN
backend/static/WarpBoxLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
@@ -52,7 +52,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. Use -1 for unlimited upload size or daily upload caps. Storage quota set to 0 means unlimited.</p>
|
<p>Blank limit fields inherit the global user defaults. Use <code>-1</code> for unlimited in any limit field — upload size, daily caps, storage quota, max expiration (the box can then last forever), daily boxes, active boxes, and short-window requests. Storage quota <code>0</code> also 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">
|
||||||
@@ -89,10 +89,10 @@
|
|||||||
<h3 class="settings-section-title">Upload limits</h3>
|
<h3 class="settings-section-title">Upload limits</h3>
|
||||||
<label><span>Max upload size (MB)</span><input name="max_upload_mb" value="{{.Data.UserEdit.MaxUploadMB}}" placeholder="inherit"></label>
|
<label><span>Max upload size (MB)</span><input name="max_upload_mb" value="{{.Data.UserEdit.MaxUploadMB}}" placeholder="inherit"></label>
|
||||||
<label><span>Daily upload cap (MB)</span><input name="daily_upload_mb" value="{{.Data.UserEdit.DailyUploadMB}}" placeholder="inherit"></label>
|
<label><span>Daily upload cap (MB)</span><input name="daily_upload_mb" value="{{.Data.UserEdit.DailyUploadMB}}" placeholder="inherit"></label>
|
||||||
<label><span>Max expiration (days)</span><input type="number" min="1" name="max_days" value="{{.Data.UserEdit.MaxDays}}" placeholder="inherit"></label>
|
<label><span>Max expiration (days)</span><input type="number" min="-1" name="max_days" value="{{.Data.UserEdit.MaxDays}}" placeholder="inherit"></label>
|
||||||
<label><span>Daily boxes</span><input type="number" min="1" name="daily_boxes" value="{{.Data.UserEdit.DailyBoxes}}" placeholder="inherit"></label>
|
<label><span>Daily boxes</span><input type="number" min="-1" name="daily_boxes" value="{{.Data.UserEdit.DailyBoxes}}" placeholder="inherit"></label>
|
||||||
<label><span>Active boxes</span><input type="number" min="1" name="active_boxes" value="{{.Data.UserEdit.ActiveBoxes}}" placeholder="inherit"></label>
|
<label><span>Active boxes</span><input type="number" min="-1" name="active_boxes" value="{{.Data.UserEdit.ActiveBoxes}}" placeholder="inherit"></label>
|
||||||
<label><span>Short-window requests</span><input type="number" min="1" name="short_window_requests" value="{{.Data.UserEdit.ShortWindowRequests}}" placeholder="inherit"></label>
|
<label><span>Short-window requests</span><input type="number" min="-1" name="short_window_requests" value="{{.Data.UserEdit.ShortWindowRequests}}" placeholder="inherit"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Save user</button>
|
<button class="button button-primary" type="submit">Save user</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user