All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
Introduce Stage 4 features to support multi-user accounts, cookie-based web sessions, and personal dashboards. Changes include: - Adding `/register` to bootstrap the first admin account and `/login`/`/logout` for session management. - Creating a personal dashboard (`/app`) to display owned boxes, storage usage, and upload history. - Implementing admin user management (`/admin/users`) for generating invite links and managing user states. - Updating the bbolt database schema to store users, sessions, invites, and collections. - Adding `golang.org/x/crypto` for password hashing and introducing unit tests for account handlers.
99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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
|
|
if !isAdminUpload {
|
|
r.Body = http.MaxBytesReader(w, r.Body, a.uploadService.MaxUploadSize()*8)
|
|
}
|
|
parseLimit := a.uploadService.MaxUploadSize() * 8
|
|
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)
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
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 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")
|
|
}
|