fix: register gob types and use RememberValue for avatar and unsplash cache
Register CachedAvatar and Photo with encoding/gob so Redis can properly deserialize them. Migrate both to use RememberValue[T] which calls GetWithValue() internally, fixing the broken type assertion when Redis is the keyvalue backend. Also removes the recursion-depth fallback in upload.go since RememberValue eliminates the type mismatch failure mode entirely.
This commit is contained in:
parent
e2de681b71
commit
59b047f76a
|
|
@ -19,6 +19,7 @@ package upload
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
|
|
@ -34,6 +35,10 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(CachedAvatar{})
|
||||
}
|
||||
|
||||
// Provider represents the upload avatar provider
|
||||
type Provider struct {
|
||||
}
|
||||
|
|
@ -53,76 +58,48 @@ type CachedAvatar struct {
|
|||
|
||||
// GetAvatar returns an uploaded user avatar
|
||||
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
||||
return p.getAvatarWithDepth(u, size, 0)
|
||||
}
|
||||
|
||||
func (p *Provider) getAvatarWithDepth(u *user.User, size int64, recursionDepth int) (avatar []byte, mimeType string, err error) {
|
||||
// Prevent infinite recursion - max 3 attempts
|
||||
if recursionDepth >= 3 {
|
||||
return nil, "", fmt.Errorf("maximum recursion depth reached while generating avatar for user %d, size %d", u.ID, size)
|
||||
}
|
||||
|
||||
cacheKey := CacheKeyPrefix + strconv.Itoa(int(u.ID)) + "_" + strconv.FormatInt(size, 10)
|
||||
|
||||
result, err := keyvalue.Remember(cacheKey, func() (any, error) {
|
||||
cachedAvatar, err := keyvalue.RememberValue(cacheKey, func() (CachedAvatar, error) {
|
||||
log.Debugf("Uploaded avatar for user %d and size %d not cached, resizing and caching.", u.ID, size)
|
||||
|
||||
// Check if user has an avatar file ID
|
||||
if u.AvatarFileID == 0 {
|
||||
return nil, fmt.Errorf("user %d has no avatar file", u.ID)
|
||||
return CachedAvatar{}, fmt.Errorf("user %d has no avatar file", u.ID)
|
||||
}
|
||||
|
||||
// If we get this far, the avatar is either not cached at all or not in this size
|
||||
f := &files.File{ID: u.AvatarFileID}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return nil, err
|
||||
return CachedAvatar{}, err
|
||||
}
|
||||
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return nil, err
|
||||
return CachedAvatar{}, err
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(f.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return CachedAvatar{}, err
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, int(size), imaging.Lanczos)
|
||||
buf := &bytes.Buffer{}
|
||||
if err := png.Encode(buf, resizedImg); err != nil {
|
||||
return nil, err
|
||||
return CachedAvatar{}, err
|
||||
}
|
||||
|
||||
avatar, err = io.ReadAll(buf)
|
||||
avatarBytes, err := io.ReadAll(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return CachedAvatar{}, err
|
||||
}
|
||||
|
||||
// Always use image/png for resized avatars since we're encoding with png
|
||||
mimeType = "image/png"
|
||||
return CachedAvatar{
|
||||
Content: avatar,
|
||||
MimeType: mimeType,
|
||||
Content: avatarBytes,
|
||||
MimeType: "image/png",
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Safe type assertion to handle cases where cached data might be corrupted or in legacy format
|
||||
cachedAvatar, ok := result.(CachedAvatar)
|
||||
if !ok {
|
||||
// Log the type mismatch with the actual stored value for debugging
|
||||
log.Errorf("Invalid cached avatar type for user %d, size %d. Expected CachedAvatar, got %T with value: %+v. Clearing cache and regenerating.", u.ID, size, result, result)
|
||||
|
||||
// Clear the invalid cache entry
|
||||
if err := keyvalue.Del(cacheKey); err != nil {
|
||||
log.Errorf("Failed to clear invalid cache entry for key %s: %v", cacheKey, err)
|
||||
}
|
||||
|
||||
// Regenerate the avatar by calling the function again (without the corrupted cache)
|
||||
return p.getAvatarWithDepth(u, size, recursionDepth+1)
|
||||
}
|
||||
|
||||
return cachedAvatar.Content, cachedAvatar.MimeType, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package unsplash
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -41,6 +42,10 @@ import (
|
|||
"code.vikunja.io/api/pkg/web"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(Photo{})
|
||||
}
|
||||
|
||||
const (
|
||||
unsplashAPIURL = `https://api.unsplash.com/`
|
||||
cachePrefix = `unsplash_photo_`
|
||||
|
|
@ -126,7 +131,7 @@ func getImageID(fullURL string) string {
|
|||
|
||||
// Gets an unsplash photo either from cache or directly from the unsplash api
|
||||
func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
|
||||
result, err := keyvalue.Remember(cachePrefix+photoID, func() (any, error) {
|
||||
p, err := keyvalue.RememberValue(cachePrefix+photoID, func() (Photo, error) {
|
||||
log.Debugf("Image information for unsplash photo %s not cached, requesting from unsplash...", photoID)
|
||||
photo := &Photo{}
|
||||
err := doGet("photos/"+photoID, photo)
|
||||
|
|
@ -136,8 +141,6 @@ func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
p := result.(Photo)
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue