package handlers import ( "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 := a.currentUser(r) 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 } 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 } 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, 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")), Password: r.FormValue("password"), ObfuscateMetadata: r.FormValue("obfuscate_metadata") == "on", OwnerID: ownerID, CollectionID: collectionID, SkipSizeLimit: isAdminUpload, }) 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); 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, 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 } 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") }