feat: add upload policies, daily limits, and storage quotas
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
- Add environment variables to configure anonymous uploads, daily upload caps, and default user storage limits. - Update config loader to parse and validate the new settings. - Implement backend logic to track daily usage and active storage per user. - Update README and `.env.example` to document the new settings and admin panels.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/helpers"
|
||||
"warpbox.dev/backend/libs/jobs"
|
||||
@@ -16,10 +17,21 @@ import (
|
||||
func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
user, loggedIn := a.currentUser(r)
|
||||
isAdminUpload := loggedIn && user.Role == services.UserRoleAdmin
|
||||
if !isAdminUpload {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, a.uploadService.MaxUploadSize()*8)
|
||||
settings, err := a.settingsService.UploadPolicy()
|
||||
if err != nil {
|
||||
a.logger.Error("failed to load upload policy", "source", "settings", "severity", "error", "code", 5005, "error", err.Error())
|
||||
helpers.WriteJSONError(w, http.StatusInternalServerError, "upload policy could not be loaded")
|
||||
return
|
||||
}
|
||||
parseLimit := a.uploadService.MaxUploadSize() * 8
|
||||
if !loggedIn && !settings.AnonymousUploadsEnabled {
|
||||
helpers.WriteJSONError(w, http.StatusForbidden, "anonymous uploads are disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdminUpload {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, uploadParseLimit(settings, loggedIn, a.uploadService.MaxUploadSize()))
|
||||
}
|
||||
parseLimit := uploadParseLimit(settings, loggedIn, a.uploadService.MaxUploadSize())
|
||||
if isAdminUpload {
|
||||
parseLimit = 32 << 20
|
||||
}
|
||||
@@ -29,6 +41,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
files := uploadFiles(r)
|
||||
totalBytes := totalUploadBytes(files)
|
||||
var ownerID string
|
||||
var collectionID string
|
||||
if loggedIn {
|
||||
@@ -39,6 +52,12 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isAdminUpload {
|
||||
if status, message := a.checkUploadPolicy(r, user, loggedIn, settings, files, totalBytes); message != "" {
|
||||
helpers.WriteJSONError(w, status, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
result, err := a.uploadService.CreateBox(files, services.UploadOptions{
|
||||
MaxDays: parseInt(r.FormValue("max_days")),
|
||||
MaxDownloads: parseInt(r.FormValue("max_downloads")),
|
||||
@@ -53,6 +72,14 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
helpers.WriteJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if !isAdminUpload {
|
||||
if err := a.recordUploadUsage(r, user, loggedIn, totalBytes); err != nil {
|
||||
a.logger.Warn("failed to record upload usage", "source", "quota", "severity", "warn", "code", 4402, "error", err.Error())
|
||||
}
|
||||
if err := a.settingsService.CleanupUsage(time.Now().UTC(), settings.UsageRetentionDays); err != nil {
|
||||
a.logger.Warn("failed to cleanup upload usage", "source", "quota", "severity", "warn", "code", 4403, "error", err.Error())
|
||||
}
|
||||
}
|
||||
jobs.GenerateThumbnailsForBoxAsync(a.uploadService, a.logger, result.BoxID)
|
||||
|
||||
if wantsJSON(r) {
|
||||
@@ -65,6 +92,76 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintln(w, result.BoxURL)
|
||||
}
|
||||
|
||||
func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bool, settings services.UploadPolicySettings, files []*multipart.FileHeader, totalBytes int64) (int, string) {
|
||||
if len(files) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if !loggedIn {
|
||||
anonymousMaxBytes := services.MegabytesToBytes(settings.AnonymousMaxUploadMB)
|
||||
for _, file := range files {
|
||||
if file.Size > anonymousMaxBytes {
|
||||
return http.StatusRequestEntityTooLarge, "file exceeds anonymous upload size limit"
|
||||
}
|
||||
}
|
||||
usage, err := a.settingsService.UsageForIP(uploadClientIP(r), now)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "upload usage could not be checked"
|
||||
}
|
||||
if usage.UploadedBytes+totalBytes > services.MegabytesToBytes(settings.AnonymousDailyUploadMB) {
|
||||
return http.StatusTooManyRequests, "anonymous daily upload limit reached"
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
usage, err := a.settingsService.UsageForUser(user.ID, now)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "upload usage could not be checked"
|
||||
}
|
||||
if usage.UploadedBytes+totalBytes > services.MegabytesToBytes(settings.UserDailyUploadMB) {
|
||||
return http.StatusTooManyRequests, "daily upload limit reached"
|
||||
}
|
||||
activeStorage, err := a.uploadService.UserActiveStorageUsed(user.ID)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "storage quota could not be checked"
|
||||
}
|
||||
quotaMB := settings.DefaultUserStorageMB
|
||||
if user.StorageQuotaMB != nil {
|
||||
quotaMB = *user.StorageQuotaMB
|
||||
}
|
||||
if activeStorage+totalBytes > services.MegabytesToBytes(quotaMB) {
|
||||
return http.StatusRequestEntityTooLarge, "storage quota reached"
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func (a *App) recordUploadUsage(r *http.Request, user services.User, loggedIn bool, totalBytes int64) error {
|
||||
now := time.Now().UTC()
|
||||
if loggedIn {
|
||||
return a.settingsService.AddUsage("user", user.ID, totalBytes, now)
|
||||
}
|
||||
return a.settingsService.AddUsage("ip", uploadClientIP(r), totalBytes, now)
|
||||
}
|
||||
|
||||
func uploadParseLimit(settings services.UploadPolicySettings, loggedIn bool, fallback int64) int64 {
|
||||
if loggedIn {
|
||||
return fallback * 8
|
||||
}
|
||||
return services.MegabytesToBytes(settings.AnonymousMaxUploadMB) * 8
|
||||
}
|
||||
|
||||
func uploadClientIP(r *http.Request) string {
|
||||
return services.ClientIP(r.RemoteAddr, r.Header.Get("X-Forwarded-For"))
|
||||
}
|
||||
|
||||
func totalUploadBytes(files []*multipart.FileHeader) int64 {
|
||||
var total int64
|
||||
for _, file := range files {
|
||||
total += file.Size
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func parseInt(value string) int {
|
||||
if value == "" {
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user