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:
kolaente 2026-04-08 10:27:30 +02:00 committed by kolaente
parent e2de681b71
commit 59b047f76a
2 changed files with 21 additions and 41 deletions

View File

@ -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
}

View File

@ -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
}