mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-26 10:17:09 +01:00
feat(artwork): add SetAvatar to the image uploader
This commit is contained in:
parent
15d6a2a728
commit
cac170781b
2 changed files with 89 additions and 0 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package artwork
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -31,6 +32,9 @@ func parseSize(value, fallback string) int64 {
|
|||
// Uploader stores a user-uploaded entity image and invalidates that entity's artwork state.
|
||||
type Uploader interface {
|
||||
SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error)
|
||||
// SetAvatar resizes a user avatar to consts.MaxAvatarSize (aspect ratio preserved,
|
||||
// no square padding) and stores it under the avatar folder.
|
||||
SetAvatar(ctx context.Context, userID, username, oldPath string, reader io.Reader, ext string) (filename string, err error)
|
||||
RemoveImage(ctx context.Context, path string) error
|
||||
// EnqueueArtwork re-resolves the item's artwork. Call it AFTER persisting the new
|
||||
// filename, or the worker resolves the old one.
|
||||
|
|
@ -77,6 +81,22 @@ func (s *uploader) SetImage(ctx context.Context, entityType string, entityID str
|
|||
return filename, nil
|
||||
}
|
||||
|
||||
func (s *uploader) SetAvatar(ctx context.Context, userID, username, oldPath string, reader io.Reader, ext string) (string, error) {
|
||||
data, err := io.ReadAll(io.LimitReader(reader, MaxImageUploadSize()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading avatar: %w", err)
|
||||
}
|
||||
// square=false: square padding would put transparent bars around a non-square photo.
|
||||
resized, _, err := resizeStaticImage(data, consts.MaxAvatarSize, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resizing avatar: %w", err)
|
||||
}
|
||||
if resized == nil {
|
||||
resized = bytes.NewReader(data) // already within bounds: resizeStaticImage returns nil, not the original
|
||||
}
|
||||
return s.SetImage(ctx, consts.EntityUser, userID, username, oldPath, resized, ext)
|
||||
}
|
||||
|
||||
func (s *uploader) EnqueueArtwork(ctx context.Context, entityType, id string) {
|
||||
kind, ok := uploadEntityKind[entityType]
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package artwork
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -93,6 +96,65 @@ var _ = Describe("Uploader", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("SetAvatar", func() {
|
||||
It("writes into the avatar folder, named after id and username", func() {
|
||||
ctx := context.Background()
|
||||
big := makePNG(1024, 1024)
|
||||
filename, err := svc.SetAvatar(ctx, "u1", "deluan", "", bytes.NewReader(big), ".png")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filename).To(Equal("u1_deluan.png"))
|
||||
Expect(filepath.Join(tmpDir, "avatar", filename)).To(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("shrinks a large image to MaxAvatarSize, preserving aspect ratio", func() {
|
||||
ctx := context.Background()
|
||||
big := makePNG(1024, 768)
|
||||
filename, err := svc.SetAvatar(ctx, "u1", "deluan", "", bytes.NewReader(big), ".png")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
f, err := os.Open(filepath.Join(tmpDir, "avatar", filename))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer f.Close()
|
||||
cfg, _, err := image.DecodeConfig(f)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg.Width).To(Equal(consts.MaxAvatarSize))
|
||||
Expect(cfg.Height).To(Equal(384)) // aspect ratio kept, not padded to a square
|
||||
})
|
||||
|
||||
It("keeps a small image intact instead of writing an empty file", func() {
|
||||
ctx := context.Background()
|
||||
small := makePNG(64, 64)
|
||||
filename, err := svc.SetAvatar(ctx, "u1", "deluan", "", bytes.NewReader(small), ".png")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(tmpDir, "avatar", filename))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(data).ToNot(BeEmpty())
|
||||
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg.Width).To(Equal(64))
|
||||
Expect(cfg.Height).To(Equal(64))
|
||||
})
|
||||
|
||||
It("removes the previous file", func() {
|
||||
ctx := context.Background()
|
||||
old := filepath.Join(tmpDir, "avatar", "u1_old.png")
|
||||
Expect(os.MkdirAll(filepath.Dir(old), 0755)).To(Succeed())
|
||||
Expect(os.WriteFile(old, []byte("x"), 0600)).To(Succeed())
|
||||
|
||||
_, err := svc.SetAvatar(ctx, "u1", "deluan", old, bytes.NewReader(makePNG(64, 64)), ".png")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(old).ToNot(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("rejects a body that is not a decodable image", func() {
|
||||
ctx := context.Background()
|
||||
_, err := svc.SetAvatar(ctx, "u1", "deluan", "", strings.NewReader("not an image"), ".png")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("EnqueueArtwork", func() {
|
||||
It("clears artwork state and enqueues a Bump", func() {
|
||||
ctx := context.Background()
|
||||
|
|
@ -172,3 +234,10 @@ var _ = Describe("MaxImageUploadSize", func() {
|
|||
Expect(MaxImageUploadSize()).To(Equal(int64(52_428_800)))
|
||||
})
|
||||
})
|
||||
|
||||
func makePNG(w, h int) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
var buf bytes.Buffer
|
||||
Expect(png.Encode(&buf, img)).To(Succeed())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue