package nativeapi import ( "context" "errors" "fmt" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "net/http" "path/filepath" "strings" "github.com/dustin/go-humanize" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" _ "golang.org/x/image/webp" ) func maxImageUploadSize() int64 { if size, err := humanize.ParseBytes(conf.Server.MaxImageUploadSize); err == nil && size > 0 { return int64(size) } size, _ := humanize.ParseBytes(consts.DefaultMaxImageUploadSize) return int64(size) } func checkImageUploadPermission(w http.ResponseWriter, r *http.Request) bool { user, _ := request.UserFrom(r.Context()) if !conf.Server.EnableArtworkUpload && !user.IsAdmin { http.Error(w, "artwork upload is disabled", http.StatusForbidden) return false } return true } func handleImageUpload(saveFn func(ctx context.Context, reader io.Reader, ext string) error) http.HandlerFunc { maxImageSize := maxImageUploadSize() return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !checkImageUploadPermission(w, r) { return } r.Body = http.MaxBytesReader(w, r.Body, maxImageSize) if err := r.ParseMultipartForm(min(maxImageSize, 10<<20)); err != nil { log.Error(ctx, "Error parsing multipart form", err) http.Error(w, "file too large or invalid form", http.StatusBadRequest) return } defer func() { if r.MultipartForm != nil { if err := r.MultipartForm.RemoveAll(); err != nil { log.Warn(ctx, "Error removing multipart temp files", err) } } }() file, header, err := r.FormFile("image") if err != nil { log.Error(ctx, "Error reading uploaded file", err) http.Error(w, "missing image file", http.StatusBadRequest) return } defer file.Close() _, format, err := image.DecodeConfig(file) if err != nil { log.Error(ctx, "Uploaded file is not a valid image", err) http.Error(w, "invalid image file", http.StatusBadRequest) return } if seeker, ok := file.(io.Seeker); ok { if _, err := seeker.Seek(0, io.SeekStart); err != nil { log.Error(ctx, "Error seeking file", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } } ext := "." + format if ext == "." { ext = strings.ToLower(filepath.Ext(header.Filename)) } if ext == "" || ext == "." { log.Error(ctx, "Could not determine image type", "filename", header.Filename) http.Error(w, "could not determine image type", http.StatusBadRequest) return } if err := saveFn(ctx, file, ext); err != nil { if errors.Is(err, model.ErrNotAuthorized) { http.Error(w, "not authorized", http.StatusForbidden) return } if errors.Is(err, model.ErrNotFound) { http.Error(w, "not found", http.StatusNotFound) return } log.Error(ctx, "Error saving image", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = fmt.Fprintf(w, `{"status":"ok"}`) } } func handleImageDelete(deleteFn func(ctx context.Context) error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !checkImageUploadPermission(w, r) { return } if err := deleteFn(ctx); err != nil { if errors.Is(err, model.ErrNotAuthorized) { http.Error(w, "not authorized", http.StatusForbidden) return } if errors.Is(err, model.ErrNotFound) { http.Error(w, "not found", http.StatusNotFound) return } log.Error(ctx, "Error removing image", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = fmt.Fprintf(w, `{"status":"ok"}`) } }