mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Relocate the image-upload service from core to core/artwork as artwork.Uploader, co-locating it with the resolver/worker/serving that own the artwork state it invalidates. MaxImageUploadSize moves too — its only callers are the two image-upload handlers — which lets core/image_upload.go be deleted entirely. Extract the shared "clear resolved state + re-queue at Bump" invalidation into artwork.Refresh and fold nativeapi's refreshArtwork handler onto it, removing the duplicated DeleteForItem+Enqueue block that had drifted into three places. The wire provider moves from core's set to artwork's; the playlists.ImageUploadService binding moves to the top-level injector so core/artwork stays unaware of playlists. Behavior is unchanged.
121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
_ "golang.org/x/image/webp"
|
|
)
|
|
|
|
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 := artwork.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"}`)
|
|
}
|
|
}
|