package handlers import ( "context" "errors" "fmt" "mime/multipart" "net/http" "strconv" "strings" "time" "warpbox.dev/backend/libs/helpers" "warpbox.dev/backend/libs/jobs" "warpbox.dev/backend/libs/services" ) func (a *App) Upload(w http.ResponseWriter, r *http.Request) { user, loggedIn, authErr := a.currentUserWithAuthError(r) if authErr != nil { helpers.WriteJSONError(w, http.StatusUnauthorized, "invalid bearer token") return } isAdminUpload := loggedIn && user.Role == services.UserRoleAdmin 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 } if !loggedIn && !settings.AnonymousUploadsEnabled { helpers.WriteJSONError(w, http.StatusForbidden, "anonymous uploads are disabled") return } effectivePolicy := a.effectiveUploadPolicy(settings, user, loggedIn) rateKey := uploadRateKey(r, user, loggedIn) if !isAdminUpload && !a.rateLimiter.Allow("upload:"+rateKey, effectivePolicy.ShortRequests, effectivePolicy.ShortWindow, time.Now().UTC()) { helpers.WriteJSONError(w, http.StatusTooManyRequests, "too many upload requests, please slow down") 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 = 32 << 20 } if err := r.ParseMultipartForm(parseLimit); err != nil { helpers.WriteJSONError(w, http.StatusBadRequest, "upload form could not be read") return } files := uploadFiles(r) totalBytes := totalUploadBytes(files) var ownerID string var collectionID string if loggedIn { ownerID = user.ID collectionID = r.FormValue("collection_id") if !a.authService.CollectionOwnedBy(collectionID, user.ID) { helpers.WriteJSONError(w, http.StatusForbidden, "collection not found") return } } if !isAdminUpload { if status, message := a.checkUploadPolicy(r, user, loggedIn, settings, effectivePolicy, files, totalBytes); message != "" { helpers.WriteJSONError(w, status, message) return } } maxDays := parseInt(r.FormValue("max_days")) if maxDays <= 0 { maxDays = min(7, effectivePolicy.MaxDays) } if !isAdminUpload && maxDays > effectivePolicy.MaxDays { helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays)) return } result, err := a.uploadService.CreateBox(files, services.UploadOptions{ MaxDays: maxDays, MaxDownloads: parseInt(r.FormValue("max_downloads")), Password: r.FormValue("password"), ObfuscateMetadata: r.FormValue("obfuscate_metadata") == "on", OwnerID: ownerID, CollectionID: collectionID, SkipSizeLimit: isAdminUpload, CreatorIP: uploadClientIP(r), StorageBackendID: effectivePolicy.StorageBackendID, }) if err != nil { a.logger.Warn("upload failed", "source", "user-upload", "severity", "warn", "code", 4001, "error", err.Error()) helpers.WriteJSONError(w, http.StatusBadRequest, err.Error()) return } if !isAdminUpload { if err := a.recordUploadUsage(r, user, loggedIn, totalBytes, 1); 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) { helpers.WriteJSON(w, http.StatusCreated, result) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusCreated) _, _ = fmt.Fprintln(w, result.BoxURL) } func (a *App) checkUploadPolicy(r *http.Request, user services.User, loggedIn bool, settings services.UploadPolicySettings, policy services.EffectiveUploadPolicy, files []*multipart.FileHeader, totalBytes int64) (int, string) { if len(files) == 0 { return 0, "" } now := time.Now().UTC() if policy.MaxUploadMB > 0 { maxBytes := services.MegabytesToBytes(policy.MaxUploadMB) for _, file := range files { if file.Size > maxBytes { return http.StatusRequestEntityTooLarge, "file exceeds upload size limit" } } } if !loggedIn { 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(policy.DailyUploadMB) { return http.StatusTooManyRequests, "anonymous daily upload limit reached" } if usage.UploadedBoxes+1 > policy.DailyBoxes { return http.StatusTooManyRequests, "anonymous daily box limit reached" } activeBoxes, err := a.uploadService.ActiveBoxCountForIP(uploadClientIP(r)) if err != nil { return http.StatusInternalServerError, "active box limit could not be checked" } if activeBoxes+1 > policy.ActiveBoxes { return http.StatusTooManyRequests, "anonymous active box limit reached" } if status, message := a.checkStorageBackendCapacity(policy.StorageBackendID, settings, totalBytes); message != "" { return status, message } 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(policy.DailyUploadMB) { return http.StatusTooManyRequests, "daily upload limit reached" } if usage.UploadedBoxes+1 > policy.DailyBoxes { return http.StatusTooManyRequests, "daily box limit reached" } activeBoxes, err := a.uploadService.ActiveBoxCountForUser(user.ID) if err != nil { return http.StatusInternalServerError, "active box limit could not be checked" } if activeBoxes+1 > policy.ActiveBoxes { return http.StatusTooManyRequests, "active box limit reached" } activeStorage, err := a.uploadService.UserActiveStorageUsed(user.ID) if err != nil { return http.StatusInternalServerError, "storage quota could not be checked" } if policy.StorageQuotaSet && activeStorage+totalBytes > services.MegabytesToBytes(policy.StorageQuotaMB) { return http.StatusRequestEntityTooLarge, "storage quota reached" } if status, message := a.checkStorageBackendCapacity(policy.StorageBackendID, settings, totalBytes); message != "" { return status, message } return 0, "" } func (a *App) recordUploadUsage(r *http.Request, user services.User, loggedIn bool, totalBytes int64, boxes int) error { now := time.Now().UTC() if loggedIn { return a.settingsService.AddUploadUsage("user", user.ID, totalBytes, boxes, now) } return a.settingsService.AddUploadUsage("ip", uploadClientIP(r), totalBytes, boxes, now) } func (a *App) effectiveUploadPolicy(settings services.UploadPolicySettings, user services.User, loggedIn bool) services.EffectiveUploadPolicy { if loggedIn { return a.settingsService.EffectivePolicyForUser(settings, user) } return a.settingsService.EffectivePolicyForAnonymous(settings) } func (a *App) checkStorageBackendCapacity(backendID string, settings services.UploadPolicySettings, totalBytes int64) (int, string) { if backendID != services.StorageBackendLocal { return 0, "" } backend, err := a.uploadService.Storage().Backend(services.StorageBackendLocal) if err != nil { return http.StatusInternalServerError, "storage backend could not be checked" } used, err := backend.Usage(context.Background()) if err != nil { return http.StatusInternalServerError, "storage backend usage could not be checked" } if used+totalBytes > services.GigabytesToBytes(settings.LocalStorageMaxGB) { return http.StatusRequestEntityTooLarge, "local storage limit reached" } return 0, "" } func uploadParseLimit(policy services.EffectiveUploadPolicy, loggedIn bool, fallback int64) int64 { if loggedIn && policy.MaxUploadMB <= 0 { return fallback * 8 } if policy.MaxUploadMB > 0 { return services.MegabytesToBytes(policy.MaxUploadMB) * 8 } return fallback * 8 } func uploadClientIP(r *http.Request) string { return services.ClientIP(r.RemoteAddr, r.Header.Get("X-Forwarded-For")) } func uploadRateKey(r *http.Request, user services.User, loggedIn bool) string { if loggedIn { return "user:" + user.ID } return "ip:" + uploadClientIP(r) } 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 } parsed, err := strconv.Atoi(value) if err != nil { return 0 } return parsed } func statusForDownloadError(err error) int { if errors.Is(err, http.ErrMissingFile) { return http.StatusNotFound } return http.StatusForbidden } func uploadFiles(r *http.Request) []*multipart.FileHeader { if r.MultipartForm == nil { return nil } files := make([]*multipart.FileHeader, 0) files = append(files, r.MultipartForm.File["file"]...) files = append(files, r.MultipartForm.File["sharex"]...) return files } func wantsJSON(r *http.Request) bool { return strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") }