Enhance avatar upload handling by providing immediate preview data and improving cache write logic

This commit is contained in:
Leon Bösche
2026-01-31 18:09:07 +01:00
parent 6085409bad
commit ff4c9bb26c
2 changed files with 38 additions and 3 deletions

View File

@@ -113,6 +113,19 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
_avatarUploadProgress = 0.0; _avatarUploadProgress = 0.0;
}); });
// Immediately show a preview if the server returned the avatar bytes
try {
final avatarData = response['avatarData'] as String?;
final contentType = response['contentType'] as String?;
if (avatarData != null && contentType != null) {
setState(() {
_avatarUrl = 'data:$contentType;base64,$avatarData';
});
}
} catch (_) {
// ignore
}
// Fetch latest profile from backend so we get the canonical avatar URL and version // Fetch latest profile from backend so we get the canonical avatar URL and version
try { try {
final apiClient = GetIt.I<ApiClient>(); final apiClient = GetIt.I<ApiClient>();

View File

@@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"context" "context"
"database/sql" "database/sql"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -84,7 +85,11 @@ func writeAvatarCache(cfg *config.Config, userID string, version string, ext str
dir = fallback dir = fallback
} }
p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext) p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext)
return os.WriteFile(p, data, 0644) if err := os.WriteFile(p, data, 0644); err != nil {
return err
}
fmt.Printf("[INFO] Wrote avatar cache for user=%s v=%s path=%s size=%d\n", userID, version, p, len(data))
return nil
} }
// readAvatarCache attempts to read a cached avatar for a user and optional version. If version is provided, it looks for an exact match; otherwise it returns the latest available cached avatar (if any). // readAvatarCache attempts to read a cached avatar for a user and optional version. If version is provided, it looks for an exact match; otherwise it returns the latest available cached avatar (if any).
@@ -96,9 +101,12 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
// Also check fallback tmp dir // Also check fallback tmp dir
checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars")) checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars"))
fmt.Printf("[DEBUG] readAvatarCache checking dirs=%v for user=%s version=%s\n", checkDirs, userID, version)
for _, dir := range checkDirs { for _, dir := range checkDirs {
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
fmt.Printf("[DEBUG] readAvatarCache cannot read dir %s: %v\n", dir, err)
continue continue
} }
if version != "" { if version != "" {
@@ -107,8 +115,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
for _, e := range entries { for _, e := range entries {
if strings.HasPrefix(e.Name(), prefix) { if strings.HasPrefix(e.Name(), prefix) {
p := filepath.Join(dir, e.Name()) p := filepath.Join(dir, e.Name())
fmt.Printf("[INFO] Found cached avatar for user=%s v=%s path=%s\n", userID, version, p)
b, err := os.ReadFile(p) b, err := os.ReadFile(p)
if err != nil { if err != nil {
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
return nil, "", err return nil, "", err
} }
ext := filepath.Ext(e.Name()) ext := filepath.Ext(e.Name())
@@ -120,6 +130,7 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
} }
} }
// not found // not found
fmt.Printf("[DEBUG] no exact cached avatar match in %s for prefix=%s\n", dir, prefix)
continue continue
} }
// no version specified: return latest by modtime of files starting with userID. // no version specified: return latest by modtime of files starting with userID.
@@ -139,8 +150,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
} }
if latest != nil { if latest != nil {
p := filepath.Join(dir, latestName) p := filepath.Join(dir, latestName)
fmt.Printf("[INFO] Found latest cached avatar for user=%s path=%s\n", userID, p)
b, err := os.ReadFile(p) b, err := os.ReadFile(p)
if err != nil { if err != nil {
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
return nil, "", err return nil, "", err
} }
ext := filepath.Ext(latestName) ext := filepath.Ext(latestName)
@@ -4213,8 +4226,11 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
} }
// Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads // Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads
versionStr := fmt.Sprintf("%d", version) versionStr := fmt.Sprintf("%d", version)
cached := false
if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil { if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil {
fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err) fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err)
} else {
cached = true
} }
// Audit log // Audit log
@@ -4228,14 +4244,20 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
}, },
}) })
// Provide avatarData for immediate preview (small images only)
avatarData := base64.StdEncoding.EncodeToString(fileBytes)
publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version) publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version)
if token, ok := middleware.GetToken(r.Context()); ok && token != "" { if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
publicURL = fmt.Sprintf("%s&token=%s", publicURL, token) publicURL = fmt.Sprintf("%s&token=%s", publicURL, token)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Avatar uploaded successfully", "message": "Avatar uploaded successfully",
"avatarUrl": publicURL, "avatarUrl": publicURL,
"cached": cached,
"avatarData": avatarData,
"contentType": contentType,
}) })
} }